2025-08-28 23:40:44 +02:00
|
|
|
import React, { useState } from 'react';
|
2025-09-20 08:59:12 +02:00
|
|
|
import { User, Mail, Phone, Lock, Globe, Clock, Camera, Save, X, Bell, MessageSquare, Smartphone, RotateCcw, CreditCard, Crown, Package, MapPin, Users, TrendingUp, Calendar, CheckCircle, AlertCircle, ArrowRight, Star, RefreshCw, Settings, Download, ExternalLink } from 'lucide-react';
|
|
|
|
|
import { Button, Card, Avatar, Input, Select, Tabs, Badge, Modal } from '../../../../components/ui';
|
2025-08-28 23:40:44 +02:00
|
|
|
import { PageHeader } from '../../../../components/layout';
|
2025-08-31 22:14:05 +02:00
|
|
|
import { useAuthUser } from '../../../../stores/auth.store';
|
2025-09-20 08:59:12 +02:00
|
|
|
import { useCurrentTenant } from '../../../../stores';
|
2025-08-31 22:14:05 +02:00
|
|
|
import { useToast } from '../../../../hooks/ui/useToast';
|
2025-09-11 18:21:32 +02:00
|
|
|
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
2025-09-20 08:59:12 +02:00
|
|
|
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
|
2025-09-21 11:57:03 +02:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2025-08-31 22:14:05 +02:00
|
|
|
|
|
|
|
|
interface ProfileFormData {
|
|
|
|
|
first_name: string;
|
|
|
|
|
last_name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
phone: string;
|
|
|
|
|
language: string;
|
|
|
|
|
timezone: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PasswordData {
|
|
|
|
|
currentPassword: string;
|
|
|
|
|
newPassword: string;
|
|
|
|
|
confirmPassword: string;
|
|
|
|
|
}
|
2025-08-28 23:40:44 +02:00
|
|
|
|
2025-09-20 08:59:12 +02:00
|
|
|
interface NotificationPreferences {
|
|
|
|
|
notifications: {
|
|
|
|
|
inventory: {
|
|
|
|
|
app: boolean;
|
|
|
|
|
email: boolean;
|
|
|
|
|
sms: boolean;
|
|
|
|
|
frequency: string;
|
|
|
|
|
};
|
|
|
|
|
sales: {
|
|
|
|
|
app: boolean;
|
|
|
|
|
email: boolean;
|
|
|
|
|
sms: boolean;
|
|
|
|
|
frequency: string;
|
|
|
|
|
};
|
|
|
|
|
production: {
|
|
|
|
|
app: boolean;
|
|
|
|
|
email: boolean;
|
|
|
|
|
sms: boolean;
|
|
|
|
|
frequency: string;
|
|
|
|
|
};
|
|
|
|
|
system: {
|
|
|
|
|
app: boolean;
|
|
|
|
|
email: boolean;
|
|
|
|
|
sms: boolean;
|
|
|
|
|
frequency: string;
|
|
|
|
|
};
|
|
|
|
|
marketing: {
|
|
|
|
|
app: boolean;
|
|
|
|
|
email: boolean;
|
|
|
|
|
sms: boolean;
|
|
|
|
|
frequency: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
global: {
|
|
|
|
|
doNotDisturb: boolean;
|
|
|
|
|
quietHours: {
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
start: string;
|
|
|
|
|
end: string;
|
|
|
|
|
};
|
|
|
|
|
language: string;
|
|
|
|
|
timezone: string;
|
|
|
|
|
soundEnabled: boolean;
|
|
|
|
|
vibrationEnabled: boolean;
|
|
|
|
|
};
|
|
|
|
|
channels: {
|
|
|
|
|
email: string;
|
|
|
|
|
phone: string;
|
|
|
|
|
slack: boolean;
|
|
|
|
|
webhook: string;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 23:40:44 +02:00
|
|
|
const ProfilePage: React.FC = () => {
|
2025-08-31 22:14:05 +02:00
|
|
|
const user = useAuthUser();
|
2025-09-21 11:57:03 +02:00
|
|
|
const { t } = useTranslation('auth');
|
2025-09-11 18:21:32 +02:00
|
|
|
const { addToast } = useToast();
|
|
|
|
|
|
|
|
|
|
const { data: profile, isLoading: profileLoading, error: profileError } = useAuthProfile();
|
|
|
|
|
const updateProfileMutation = useUpdateProfile();
|
|
|
|
|
const changePasswordMutation = useChangePassword();
|
2025-08-31 22:14:05 +02:00
|
|
|
|
2025-08-28 23:40:44 +02:00
|
|
|
const [isEditing, setIsEditing] = useState(false);
|
2025-08-31 22:14:05 +02:00
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
2025-09-20 08:59:12 +02:00
|
|
|
const [activeTab, setActiveTab] = useState('profile');
|
|
|
|
|
const [hasPreferencesChanges, setHasPreferencesChanges] = useState(false);
|
|
|
|
|
const [usageSummary, setUsageSummary] = useState<UsageSummary | null>(null);
|
|
|
|
|
const [availablePlans, setAvailablePlans] = useState<AvailablePlans | null>(null);
|
|
|
|
|
const [subscriptionLoading, setSubscriptionLoading] = useState(false);
|
|
|
|
|
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
|
|
|
|
const [selectedPlan, setSelectedPlan] = useState<string>('');
|
|
|
|
|
const [upgrading, setUpgrading] = useState(false);
|
|
|
|
|
const currentTenant = useCurrentTenant();
|
2025-08-31 22:14:05 +02:00
|
|
|
|
|
|
|
|
const [profileData, setProfileData] = useState<ProfileFormData>({
|
2025-09-11 18:21:32 +02:00
|
|
|
first_name: '',
|
|
|
|
|
last_name: '',
|
|
|
|
|
email: '',
|
|
|
|
|
phone: '',
|
2025-08-31 22:14:05 +02:00
|
|
|
language: 'es',
|
|
|
|
|
timezone: 'Europe/Madrid'
|
|
|
|
|
});
|
2025-09-11 18:21:32 +02:00
|
|
|
|
|
|
|
|
// Update profile data when profile is loaded
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (profile) {
|
|
|
|
|
setProfileData({
|
|
|
|
|
first_name: profile.first_name || '',
|
|
|
|
|
last_name: profile.last_name || '',
|
|
|
|
|
email: profile.email || '',
|
|
|
|
|
phone: profile.phone || '',
|
|
|
|
|
language: profile.language || 'es',
|
|
|
|
|
timezone: profile.timezone || 'Europe/Madrid'
|
|
|
|
|
});
|
2025-09-20 08:59:12 +02:00
|
|
|
|
|
|
|
|
// Update preferences with profile data
|
|
|
|
|
setPreferences(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
global: {
|
|
|
|
|
...prev.global,
|
|
|
|
|
language: profile.language || 'es',
|
|
|
|
|
timezone: profile.timezone || 'Europe/Madrid'
|
|
|
|
|
},
|
|
|
|
|
channels: {
|
|
|
|
|
...prev.channels,
|
|
|
|
|
email: profile.email || '',
|
|
|
|
|
phone: profile.phone || ''
|
|
|
|
|
}
|
|
|
|
|
}));
|
2025-09-11 18:21:32 +02:00
|
|
|
}
|
|
|
|
|
}, [profile]);
|
2025-08-31 22:14:05 +02:00
|
|
|
|
2025-09-20 08:59:12 +02:00
|
|
|
// Load subscription data when needed
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (activeTab === 'subscription' && (currentTenant?.id || user?.tenant_id) && !usageSummary) {
|
|
|
|
|
loadSubscriptionData();
|
|
|
|
|
}
|
|
|
|
|
}, [activeTab, currentTenant, user?.tenant_id]);
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const [passwordData, setPasswordData] = useState<PasswordData>({
|
|
|
|
|
currentPassword: '',
|
|
|
|
|
newPassword: '',
|
|
|
|
|
confirmPassword: ''
|
2025-08-28 23:40:44 +02:00
|
|
|
});
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
|
|
2025-09-20 08:59:12 +02:00
|
|
|
const [preferences, setPreferences] = useState<NotificationPreferences>({
|
|
|
|
|
notifications: {
|
|
|
|
|
inventory: {
|
|
|
|
|
app: true,
|
|
|
|
|
email: false,
|
|
|
|
|
sms: true,
|
|
|
|
|
frequency: 'immediate'
|
|
|
|
|
},
|
|
|
|
|
sales: {
|
|
|
|
|
app: true,
|
|
|
|
|
email: true,
|
|
|
|
|
sms: false,
|
|
|
|
|
frequency: 'hourly'
|
|
|
|
|
},
|
|
|
|
|
production: {
|
|
|
|
|
app: true,
|
|
|
|
|
email: false,
|
|
|
|
|
sms: true,
|
|
|
|
|
frequency: 'immediate'
|
|
|
|
|
},
|
|
|
|
|
system: {
|
|
|
|
|
app: true,
|
|
|
|
|
email: true,
|
|
|
|
|
sms: false,
|
|
|
|
|
frequency: 'daily'
|
|
|
|
|
},
|
|
|
|
|
marketing: {
|
|
|
|
|
app: false,
|
|
|
|
|
email: true,
|
|
|
|
|
sms: false,
|
|
|
|
|
frequency: 'weekly'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
global: {
|
|
|
|
|
doNotDisturb: false,
|
|
|
|
|
quietHours: {
|
|
|
|
|
enabled: false,
|
|
|
|
|
start: '22:00',
|
|
|
|
|
end: '07:00'
|
|
|
|
|
},
|
|
|
|
|
language: 'es',
|
|
|
|
|
timezone: 'Europe/Madrid',
|
|
|
|
|
soundEnabled: true,
|
|
|
|
|
vibrationEnabled: true
|
|
|
|
|
},
|
|
|
|
|
channels: {
|
|
|
|
|
email: '',
|
|
|
|
|
phone: '',
|
|
|
|
|
slack: false,
|
|
|
|
|
webhook: ''
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const languageOptions = [
|
|
|
|
|
{ value: 'es', label: 'Español' },
|
|
|
|
|
{ value: 'ca', label: 'Català' },
|
|
|
|
|
{ value: 'en', label: 'English' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const timezoneOptions = [
|
|
|
|
|
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
|
|
|
|
|
{ value: 'Atlantic/Canary', label: 'Canarias (WET/WEST)' },
|
|
|
|
|
{ value: 'Europe/London', label: 'Londres (GMT/BST)' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const validateProfile = (): boolean => {
|
|
|
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
if (!profileData.first_name.trim()) {
|
|
|
|
|
newErrors.first_name = 'El nombre es requerido';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!profileData.last_name.trim()) {
|
|
|
|
|
newErrors.last_name = 'Los apellidos son requeridos';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!profileData.email.trim()) {
|
|
|
|
|
newErrors.email = 'El email es requerido';
|
|
|
|
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profileData.email)) {
|
|
|
|
|
newErrors.email = 'Email inválido';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors);
|
|
|
|
|
return Object.keys(newErrors).length === 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const validatePassword = (): boolean => {
|
|
|
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
if (!passwordData.currentPassword) {
|
|
|
|
|
newErrors.currentPassword = 'Contraseña actual requerida';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!passwordData.newPassword) {
|
|
|
|
|
newErrors.newPassword = 'Nueva contraseña requerida';
|
|
|
|
|
} else if (passwordData.newPassword.length < 8) {
|
|
|
|
|
newErrors.newPassword = 'Mínimo 8 caracteres';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
|
|
|
|
newErrors.confirmPassword = 'Las contraseñas no coinciden';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors);
|
|
|
|
|
return Object.keys(newErrors).length === 0;
|
2025-08-28 23:40:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const handleSaveProfile = async () => {
|
|
|
|
|
if (!validateProfile()) return;
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-11 18:21:32 +02:00
|
|
|
await updateProfileMutation.mutateAsync(profileData);
|
2025-08-31 22:14:05 +02:00
|
|
|
|
|
|
|
|
setIsEditing(false);
|
2025-09-11 18:21:32 +02:00
|
|
|
addToast('Perfil actualizado correctamente', 'success');
|
2025-08-31 22:14:05 +02:00
|
|
|
} catch (error) {
|
2025-09-11 18:21:32 +02:00
|
|
|
addToast('No se pudo actualizar tu perfil', 'error');
|
2025-08-31 22:14:05 +02:00
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
2025-08-28 23:40:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
const handleChangePasswordSubmit = async () => {
|
2025-08-31 22:14:05 +02:00
|
|
|
if (!validatePassword()) return;
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-11 18:21:32 +02:00
|
|
|
await changePasswordMutation.mutateAsync({
|
|
|
|
|
current_password: passwordData.currentPassword,
|
|
|
|
|
new_password: passwordData.newPassword,
|
|
|
|
|
confirm_password: passwordData.confirmPassword
|
|
|
|
|
});
|
2025-08-31 22:14:05 +02:00
|
|
|
|
|
|
|
|
setShowPasswordForm(false);
|
|
|
|
|
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
2025-09-11 18:21:32 +02:00
|
|
|
addToast('Contraseña actualizada correctamente', 'success');
|
2025-08-31 22:14:05 +02:00
|
|
|
} catch (error) {
|
2025-09-11 18:21:32 +02:00
|
|
|
addToast('No se pudo cambiar tu contraseña', 'error');
|
2025-08-31 22:14:05 +02:00
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
2025-08-28 23:40:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const handleInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
setProfileData(prev => ({ ...prev, [field]: e.target.value }));
|
|
|
|
|
if (errors[field]) {
|
|
|
|
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
|
|
|
}
|
2025-08-28 23:40:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const handleSelectChange = (field: keyof ProfileFormData) => (value: string) => {
|
|
|
|
|
setProfileData(prev => ({ ...prev, [field]: value }));
|
2025-08-28 23:40:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
const handlePasswordChange = (field: keyof PasswordData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
setPasswordData(prev => ({ ...prev, [field]: e.target.value }));
|
|
|
|
|
if (errors[field]) {
|
|
|
|
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
|
|
|
}
|
2025-08-28 23:40:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-20 08:59:12 +02:00
|
|
|
// Communication Preferences handlers
|
|
|
|
|
const categories = [
|
|
|
|
|
{
|
|
|
|
|
id: 'inventory',
|
|
|
|
|
name: 'Inventario',
|
|
|
|
|
description: 'Alertas de stock, reposiciones y vencimientos',
|
|
|
|
|
icon: '📦'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'sales',
|
|
|
|
|
name: 'Ventas',
|
|
|
|
|
description: 'Pedidos, transacciones y reportes de ventas',
|
|
|
|
|
icon: '💰'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'production',
|
|
|
|
|
name: 'Producción',
|
|
|
|
|
description: 'Hornadas, calidad y tiempos de producción',
|
|
|
|
|
icon: '🍞'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'system',
|
|
|
|
|
name: 'Sistema',
|
|
|
|
|
description: 'Actualizaciones, mantenimiento y errores',
|
|
|
|
|
icon: '⚙️'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'marketing',
|
|
|
|
|
name: 'Marketing',
|
|
|
|
|
description: 'Campañas, promociones y análisis',
|
|
|
|
|
icon: '📢'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const frequencies = [
|
|
|
|
|
{ value: 'immediate', label: 'Inmediato' },
|
|
|
|
|
{ value: 'hourly', label: 'Cada hora' },
|
|
|
|
|
{ value: 'daily', label: 'Diario' },
|
|
|
|
|
{ value: 'weekly', label: 'Semanal' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
|
|
|
|
|
setPreferences(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
notifications: {
|
|
|
|
|
...prev.notifications,
|
|
|
|
|
[category]: {
|
|
|
|
|
...prev.notifications[category as keyof typeof prev.notifications],
|
|
|
|
|
[channel]: value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
setHasPreferencesChanges(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFrequencyChange = (category: string, frequency: string) => {
|
|
|
|
|
setPreferences(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
notifications: {
|
|
|
|
|
...prev.notifications,
|
|
|
|
|
[category]: {
|
|
|
|
|
...prev.notifications[category as keyof typeof prev.notifications],
|
|
|
|
|
frequency
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
setHasPreferencesChanges(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGlobalChange = (setting: string, value: any) => {
|
|
|
|
|
setPreferences(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
global: {
|
|
|
|
|
...prev.global,
|
|
|
|
|
[setting]: value
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
setHasPreferencesChanges(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleChannelChange = (channel: string, value: string | boolean) => {
|
|
|
|
|
setPreferences(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
channels: {
|
|
|
|
|
...prev.channels,
|
|
|
|
|
[channel]: value
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
setHasPreferencesChanges(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSavePreferences = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await updateProfileMutation.mutateAsync({
|
|
|
|
|
language: preferences.global.language,
|
|
|
|
|
timezone: preferences.global.timezone,
|
|
|
|
|
phone: preferences.channels.phone,
|
|
|
|
|
notification_preferences: preferences.notifications
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
addToast('Preferencias guardadas correctamente', 'success');
|
|
|
|
|
setHasPreferencesChanges(false);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast('Error al guardar las preferencias', 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleResetPreferences = () => {
|
|
|
|
|
if (profile) {
|
|
|
|
|
setPreferences({
|
|
|
|
|
notifications: {
|
|
|
|
|
inventory: { app: true, email: false, sms: true, frequency: 'immediate' },
|
|
|
|
|
sales: { app: true, email: true, sms: false, frequency: 'hourly' },
|
|
|
|
|
production: { app: true, email: false, sms: true, frequency: 'immediate' },
|
|
|
|
|
system: { app: true, email: true, sms: false, frequency: 'daily' },
|
|
|
|
|
marketing: { app: false, email: true, sms: false, frequency: 'weekly' }
|
|
|
|
|
},
|
|
|
|
|
global: {
|
|
|
|
|
doNotDisturb: false,
|
|
|
|
|
quietHours: { enabled: false, start: '22:00', end: '07:00' },
|
|
|
|
|
language: profile.language || 'es',
|
|
|
|
|
timezone: profile.timezone || 'Europe/Madrid',
|
|
|
|
|
soundEnabled: true,
|
|
|
|
|
vibrationEnabled: true
|
|
|
|
|
},
|
|
|
|
|
channels: {
|
|
|
|
|
email: profile.email || '',
|
|
|
|
|
phone: profile.phone || '',
|
|
|
|
|
slack: false,
|
|
|
|
|
webhook: ''
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setHasPreferencesChanges(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getChannelIcon = (channel: string) => {
|
|
|
|
|
switch (channel) {
|
|
|
|
|
case 'app':
|
|
|
|
|
return <Bell className="w-4 h-4" />;
|
|
|
|
|
case 'email':
|
|
|
|
|
return <Mail className="w-4 h-4" />;
|
|
|
|
|
case 'sms':
|
|
|
|
|
return <Smartphone className="w-4 h-4" />;
|
|
|
|
|
default:
|
|
|
|
|
return <MessageSquare className="w-4 h-4" />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Subscription handlers
|
|
|
|
|
const loadSubscriptionData = async () => {
|
|
|
|
|
let tenantId = currentTenant?.id || user?.tenant_id;
|
|
|
|
|
|
|
|
|
|
if (!tenantId) {
|
|
|
|
|
addToast('No se encontró información del tenant', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setSubscriptionLoading(true);
|
|
|
|
|
const [usage, plans] = await Promise.all([
|
|
|
|
|
subscriptionService.getUsageSummary(tenantId),
|
|
|
|
|
subscriptionService.getAvailablePlans()
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
setUsageSummary(usage);
|
|
|
|
|
setAvailablePlans(plans);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading subscription data:', error);
|
|
|
|
|
addToast("No se pudo cargar la información de suscripción", 'error');
|
|
|
|
|
} finally {
|
|
|
|
|
setSubscriptionLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpgradeClick = (planKey: string) => {
|
|
|
|
|
setSelectedPlan(planKey);
|
|
|
|
|
setUpgradeDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpgradeConfirm = async () => {
|
|
|
|
|
let tenantId = currentTenant?.id || user?.tenant_id;
|
|
|
|
|
|
|
|
|
|
if (!tenantId || !selectedPlan) {
|
|
|
|
|
addToast('Información de tenant no disponible', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setUpgrading(true);
|
|
|
|
|
|
|
|
|
|
const validation = await subscriptionService.validatePlanUpgrade(
|
|
|
|
|
tenantId,
|
|
|
|
|
selectedPlan
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!validation.can_upgrade) {
|
|
|
|
|
addToast(validation.reason || 'No se puede actualizar el plan', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
addToast(result.message, 'success');
|
|
|
|
|
|
|
|
|
|
await loadSubscriptionData();
|
|
|
|
|
setUpgradeDialogOpen(false);
|
|
|
|
|
setSelectedPlan('');
|
|
|
|
|
} else {
|
|
|
|
|
addToast('Error al cambiar el plan', 'error');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error upgrading plan:', error);
|
|
|
|
|
addToast('Error al procesar el cambio de plan', 'error');
|
|
|
|
|
} finally {
|
|
|
|
|
setUpgrading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ProgressBar: React.FC<{ value: number; className?: string }> = ({ value, className = '' }) => {
|
|
|
|
|
const getProgressColor = () => {
|
|
|
|
|
if (value >= 90) return 'bg-red-500';
|
|
|
|
|
if (value >= 80) return 'bg-yellow-500';
|
|
|
|
|
return 'bg-green-500';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`w-full bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-full h-3 ${className}`}>
|
|
|
|
|
<div
|
|
|
|
|
className={`${getProgressColor()} h-full rounded-full transition-all duration-500 relative`}
|
|
|
|
|
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
|
|
|
|
|
>
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-white/20 rounded-full"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const tabItems = [
|
|
|
|
|
{ id: 'profile', label: 'Información Personal' },
|
|
|
|
|
{ id: 'preferences', label: 'Preferencias de Comunicación' },
|
|
|
|
|
{ id: 'subscription', label: 'Suscripción y Facturación' }
|
|
|
|
|
];
|
|
|
|
|
|
2025-08-28 23:40:44 +02:00
|
|
|
return (
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
<PageHeader
|
|
|
|
|
title="Mi Perfil"
|
2025-09-20 08:59:12 +02:00
|
|
|
description="Gestiona tu información personal y preferencias de comunicación"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Tab Navigation */}
|
|
|
|
|
<Tabs
|
|
|
|
|
items={tabItems}
|
|
|
|
|
activeTab={activeTab}
|
|
|
|
|
onTabChange={setActiveTab}
|
|
|
|
|
fullWidth={true}
|
|
|
|
|
variant="pills"
|
|
|
|
|
size="md"
|
2025-08-28 23:40:44 +02:00
|
|
|
/>
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
{/* Profile Header */}
|
2025-09-20 08:59:12 +02:00
|
|
|
{activeTab === 'profile' && (
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex items-center gap-6">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Avatar
|
|
|
|
|
src={profile?.avatar_url}
|
|
|
|
|
name={`${profileData.first_name} ${profileData.last_name}`}
|
|
|
|
|
size="xl"
|
|
|
|
|
className="w-20 h-20"
|
|
|
|
|
/>
|
|
|
|
|
<button className="absolute -bottom-1 -right-1 bg-color-primary text-white rounded-full p-2 shadow-lg hover:bg-color-primary-dark transition-colors">
|
|
|
|
|
<Camera className="w-4 h-4" />
|
|
|
|
|
</button>
|
2025-08-28 23:40:44 +02:00
|
|
|
</div>
|
2025-09-20 08:59:12 +02:00
|
|
|
<div className="flex-1">
|
|
|
|
|
<h1 className="text-2xl font-bold text-text-primary mb-1">
|
|
|
|
|
{profileData.first_name} {profileData.last_name}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-text-secondary">{profileData.email}</p>
|
2025-09-21 11:57:03 +02:00
|
|
|
{user?.role && (
|
|
|
|
|
<p className="text-sm text-text-tertiary mt-1">
|
|
|
|
|
{t(`global_roles.${user.role}`)}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-09-20 08:59:12 +02:00
|
|
|
<div className="flex items-center gap-2 mt-2">
|
|
|
|
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
|
|
|
<span className="text-sm text-text-tertiary">En línea</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
{!isEditing && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setIsEditing(true)}
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<User className="w-4 h-4" />
|
|
|
|
|
Editar Perfil
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-08-31 22:14:05 +02:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
2025-09-20 08:59:12 +02:00
|
|
|
onClick={() => setShowPasswordForm(!showPasswordForm)}
|
2025-08-31 22:14:05 +02:00
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
2025-09-20 08:59:12 +02:00
|
|
|
<Lock className="w-4 h-4" />
|
|
|
|
|
Cambiar Contraseña
|
2025-08-31 22:14:05 +02:00
|
|
|
</Button>
|
2025-09-20 08:59:12 +02:00
|
|
|
</div>
|
2025-08-28 23:40:44 +02:00
|
|
|
</div>
|
2025-09-20 08:59:12 +02:00
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-08-28 23:40:44 +02:00
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
{/* Profile Form */}
|
2025-09-20 08:59:12 +02:00
|
|
|
{activeTab === 'profile' && (
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h2 className="text-lg font-semibold mb-4">Información Personal</h2>
|
2025-08-31 22:14:05 +02:00
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
|
|
|
<Input
|
|
|
|
|
label="Nombre"
|
|
|
|
|
value={profileData.first_name}
|
|
|
|
|
onChange={handleInputChange('first_name')}
|
|
|
|
|
error={errors.first_name}
|
|
|
|
|
disabled={!isEditing || isLoading}
|
|
|
|
|
leftIcon={<User className="w-4 h-4" />}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
label="Apellidos"
|
|
|
|
|
value={profileData.last_name}
|
|
|
|
|
onChange={handleInputChange('last_name')}
|
|
|
|
|
error={errors.last_name}
|
|
|
|
|
disabled={!isEditing || isLoading}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
type="email"
|
|
|
|
|
label="Correo Electrónico"
|
|
|
|
|
value={profileData.email}
|
|
|
|
|
onChange={handleInputChange('email')}
|
|
|
|
|
error={errors.email}
|
|
|
|
|
disabled={!isEditing || isLoading}
|
|
|
|
|
leftIcon={<Mail className="w-4 h-4" />}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
type="tel"
|
|
|
|
|
label="Teléfono"
|
|
|
|
|
value={profileData.phone}
|
|
|
|
|
onChange={handleInputChange('phone')}
|
|
|
|
|
error={errors.phone}
|
|
|
|
|
disabled={!isEditing || isLoading}
|
|
|
|
|
placeholder="+34 600 000 000"
|
|
|
|
|
leftIcon={<Phone className="w-4 h-4" />}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Select
|
|
|
|
|
label="Idioma"
|
|
|
|
|
options={languageOptions}
|
|
|
|
|
value={profileData.language}
|
|
|
|
|
onChange={handleSelectChange('language')}
|
|
|
|
|
disabled={!isEditing || isLoading}
|
|
|
|
|
leftIcon={<Globe className="w-4 h-4" />}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Select
|
|
|
|
|
label="Zona Horaria"
|
|
|
|
|
options={timezoneOptions}
|
|
|
|
|
value={profileData.timezone}
|
|
|
|
|
onChange={handleSelectChange('timezone')}
|
|
|
|
|
disabled={!isEditing || isLoading}
|
|
|
|
|
leftIcon={<Clock className="w-4 h-4" />}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isEditing && (
|
|
|
|
|
<div className="flex gap-3 mt-6 pt-4 border-t">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setIsEditing(false)}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
Cancelar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={handleSaveProfile}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
loadingText="Guardando..."
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<Save className="w-4 h-4" />
|
|
|
|
|
Guardar Cambios
|
|
|
|
|
</Button>
|
2025-08-28 23:40:44 +02:00
|
|
|
</div>
|
2025-08-31 22:14:05 +02:00
|
|
|
)}
|
2025-09-20 08:59:12 +02:00
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-08-28 23:40:44 +02:00
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
{/* Password Change Form */}
|
2025-09-20 08:59:12 +02:00
|
|
|
{activeTab === 'profile' && showPasswordForm && (
|
2025-08-31 22:14:05 +02:00
|
|
|
<Card className="p-6">
|
|
|
|
|
<h2 className="text-lg font-semibold mb-4">Cambiar Contraseña</h2>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-4xl">
|
|
|
|
|
<Input
|
|
|
|
|
type="password"
|
|
|
|
|
label="Contraseña Actual"
|
|
|
|
|
value={passwordData.currentPassword}
|
|
|
|
|
onChange={handlePasswordChange('currentPassword')}
|
|
|
|
|
error={errors.currentPassword}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
leftIcon={<Lock className="w-4 h-4" />}
|
|
|
|
|
/>
|
2025-08-28 23:40:44 +02:00
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
<Input
|
|
|
|
|
type="password"
|
|
|
|
|
label="Nueva Contraseña"
|
|
|
|
|
value={passwordData.newPassword}
|
|
|
|
|
onChange={handlePasswordChange('newPassword')}
|
|
|
|
|
error={errors.newPassword}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
leftIcon={<Lock className="w-4 h-4" />}
|
|
|
|
|
/>
|
2025-08-28 23:40:44 +02:00
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
<Input
|
|
|
|
|
type="password"
|
|
|
|
|
label="Confirmar Nueva Contraseña"
|
|
|
|
|
value={passwordData.confirmPassword}
|
|
|
|
|
onChange={handlePasswordChange('confirmPassword')}
|
|
|
|
|
error={errors.confirmPassword}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
leftIcon={<Lock className="w-4 h-4" />}
|
|
|
|
|
/>
|
2025-08-28 23:40:44 +02:00
|
|
|
</div>
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="flex gap-3 pt-6 mt-6 border-t">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowPasswordForm(false);
|
|
|
|
|
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
|
|
|
|
setErrors({});
|
|
|
|
|
}}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
>
|
|
|
|
|
Cancelar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
2025-09-11 18:21:32 +02:00
|
|
|
onClick={handleChangePasswordSubmit}
|
2025-08-31 22:14:05 +02:00
|
|
|
isLoading={isLoading}
|
|
|
|
|
loadingText="Cambiando..."
|
|
|
|
|
>
|
|
|
|
|
Cambiar Contraseña
|
|
|
|
|
</Button>
|
2025-08-28 23:40:44 +02:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-09-20 08:59:12 +02:00
|
|
|
|
|
|
|
|
{/* Communication Preferences Tab */}
|
|
|
|
|
{activeTab === 'preferences' && (
|
|
|
|
|
<>
|
|
|
|
|
{/* Action Buttons */}
|
|
|
|
|
<div className="flex justify-end space-x-2">
|
|
|
|
|
<Button variant="outline" onClick={handleResetPreferences}>
|
|
|
|
|
<RotateCcw className="w-4 h-4 mr-2" />
|
|
|
|
|
Restaurar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={handleSavePreferences} disabled={!hasPreferencesChanges}>
|
|
|
|
|
<Save className="w-4 h-4 mr-2" />
|
|
|
|
|
Guardar Cambios
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Global Settings */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Configuración General</h3>
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="flex items-center space-x-2">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={preferences.global.doNotDisturb}
|
|
|
|
|
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
|
|
|
|
|
className="rounded border-[var(--border-secondary)]"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm font-medium text-[var(--text-secondary)]">No molestar</span>
|
|
|
|
|
</label>
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">Silencia todas las notificaciones</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="flex items-center space-x-2">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={preferences.global.soundEnabled}
|
|
|
|
|
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
|
|
|
|
|
className="rounded border-[var(--border-secondary)]"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm font-medium text-[var(--text-secondary)]">Sonidos</span>
|
|
|
|
|
</label>
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">Reproducir sonidos de notificación</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="flex items-center space-x-2 mb-2">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={preferences.global.quietHours.enabled}
|
|
|
|
|
onChange={(e) => handleGlobalChange('quietHours', {
|
|
|
|
|
...preferences.global.quietHours,
|
|
|
|
|
enabled: e.target.checked
|
|
|
|
|
})}
|
|
|
|
|
className="rounded border-[var(--border-secondary)]"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm font-medium text-[var(--text-secondary)]">Horas silenciosas</span>
|
|
|
|
|
</label>
|
|
|
|
|
{preferences.global.quietHours.enabled && (
|
|
|
|
|
<div className="flex space-x-4 ml-6">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Desde</label>
|
|
|
|
|
<input
|
|
|
|
|
type="time"
|
|
|
|
|
value={preferences.global.quietHours.start}
|
|
|
|
|
onChange={(e) => handleGlobalChange('quietHours', {
|
|
|
|
|
...preferences.global.quietHours,
|
|
|
|
|
start: e.target.value
|
|
|
|
|
})}
|
|
|
|
|
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Hasta</label>
|
|
|
|
|
<input
|
|
|
|
|
type="time"
|
|
|
|
|
value={preferences.global.quietHours.end}
|
|
|
|
|
onChange={(e) => handleGlobalChange('quietHours', {
|
|
|
|
|
...preferences.global.quietHours,
|
|
|
|
|
end: e.target.value
|
|
|
|
|
})}
|
|
|
|
|
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Channel Settings */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Canales de Comunicación</h3>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Email</label>
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
value={preferences.channels.email}
|
|
|
|
|
onChange={(e) => handleChannelChange('email', e.target.value)}
|
|
|
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
|
|
|
placeholder="tu-email@ejemplo.com"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Teléfono (SMS)</label>
|
|
|
|
|
<input
|
|
|
|
|
type="tel"
|
|
|
|
|
value={preferences.channels.phone}
|
|
|
|
|
onChange={(e) => handleChannelChange('phone', e.target.value)}
|
|
|
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
|
|
|
placeholder="+34 600 123 456"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Webhook URL</label>
|
|
|
|
|
<input
|
|
|
|
|
type="url"
|
|
|
|
|
value={preferences.channels.webhook}
|
|
|
|
|
onChange={(e) => handleChannelChange('webhook', e.target.value)}
|
|
|
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
|
|
|
placeholder="https://tu-webhook.com/notifications"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">URL para recibir notificaciones JSON</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Category Preferences */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{categories.map((category) => {
|
|
|
|
|
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card key={category.id} className="p-6">
|
|
|
|
|
<div className="flex items-start space-x-4">
|
|
|
|
|
<div className="text-2xl">{category.icon}</div>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">{category.name}</h3>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)] mb-4">{category.description}</p>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Channel toggles */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Canales</h4>
|
|
|
|
|
<div className="flex space-x-6">
|
|
|
|
|
{['app', 'email', 'sms'].map((channel) => (
|
|
|
|
|
<label key={channel} className="flex items-center space-x-2">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
|
|
|
|
|
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
|
|
|
|
|
className="rounded border-[var(--border-secondary)]"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
{getChannelIcon(channel)}
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)] capitalize">{channel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Frequency */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia</h4>
|
|
|
|
|
<select
|
|
|
|
|
value={categoryPrefs.frequency}
|
|
|
|
|
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
|
|
|
|
|
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md text-sm"
|
|
|
|
|
>
|
|
|
|
|
{frequencies.map((freq) => (
|
|
|
|
|
<option key={freq.value} value={freq.value}>
|
|
|
|
|
{freq.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Save Changes Banner */}
|
|
|
|
|
{hasPreferencesChanges && (
|
|
|
|
|
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
|
|
|
|
<span className="text-sm">Tienes cambios sin guardar</span>
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleResetPreferences}>
|
|
|
|
|
Descartar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSavePreferences}>
|
|
|
|
|
Guardar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Subscription Tab */}
|
|
|
|
|
{activeTab === 'subscription' && (
|
|
|
|
|
<>
|
|
|
|
|
{subscriptionLoading ? (
|
|
|
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<RefreshCw className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
|
|
|
<p className="text-[var(--text-secondary)]">Cargando información de suscripción...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : !usageSummary || !availablePlans ? (
|
|
|
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<AlertCircle className="w-12 h-12 text-[var(--text-tertiary)]" />
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No se pudo cargar la información</h3>
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-4">Hubo un problema al cargar los datos de suscripción</p>
|
|
|
|
|
<Button onClick={loadSubscriptionData} variant="primary">
|
|
|
|
|
<RefreshCw className="w-4 h-4 mr-2" />
|
|
|
|
|
Reintentar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* Current Plan Overview */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] flex items-center">
|
|
|
|
|
<Crown className="w-5 h-5 mr-2 text-yellow-500" />
|
|
|
|
|
Plan Actual: {subscriptionService.getPlanDisplayInfo(usageSummary.plan).name}
|
|
|
|
|
</h3>
|
|
|
|
|
<Badge
|
|
|
|
|
variant={usageSummary.status === 'active' ? 'success' : 'default'}
|
|
|
|
|
className="text-sm font-medium"
|
|
|
|
|
>
|
|
|
|
|
{usageSummary.status === 'active' ? 'Activo' : usageSummary.status}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
|
|
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Precio Mensual</span>
|
|
|
|
|
<span className="font-semibold text-[var(--text-primary)]">{subscriptionService.formatPrice(usageSummary.monthly_price)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Próxima Facturación</span>
|
|
|
|
|
<span className="font-medium text-[var(--text-primary)]">
|
|
|
|
|
{new Date(usageSummary.next_billing_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Usuarios</span>
|
|
|
|
|
<span className="font-medium text-[var(--text-primary)]">
|
|
|
|
|
{usageSummary.usage.users.current}/{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Ubicaciones</span>
|
|
|
|
|
<span className="font-medium text-[var(--text-primary)]">
|
|
|
|
|
{usageSummary.usage.locations.current}/{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<Button variant="outline" onClick={() => window.open('https://billing.bakery.com', '_blank')} className="flex items-center gap-2">
|
|
|
|
|
<ExternalLink className="w-4 h-4" />
|
|
|
|
|
Portal de Facturación
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline" onClick={() => console.log('Download invoice')} className="flex items-center gap-2">
|
|
|
|
|
<Download className="w-4 h-4" />
|
|
|
|
|
Descargar Facturas
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline" onClick={loadSubscriptionData} className="flex items-center gap-2">
|
|
|
|
|
<RefreshCw className="w-4 h-4" />
|
|
|
|
|
Actualizar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Usage Details */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold mb-6 text-[var(--text-primary)] flex items-center">
|
|
|
|
|
<TrendingUp className="w-5 h-5 mr-2 text-orange-500" />
|
|
|
|
|
Uso de Recursos
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
|
{/* Users */}
|
|
|
|
|
<div className="space-y-4 p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="p-2 bg-blue-500/10 rounded-lg border border-blue-500/20">
|
|
|
|
|
<Users className="w-4 h-4 text-blue-500" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="font-medium text-[var(--text-primary)]">Usuarios</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-bold text-[var(--text-primary)]">
|
|
|
|
|
{usageSummary.usage.users.current}<span className="text-[var(--text-tertiary)]">/</span>
|
|
|
|
|
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<ProgressBar value={usageSummary.usage.users.usage_percentage} />
|
|
|
|
|
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
|
|
|
|
<span>{usageSummary.usage.users.usage_percentage}% utilizado</span>
|
|
|
|
|
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : `${usageSummary.usage.users.limit - usageSummary.usage.users.current} restantes`}</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Locations */}
|
|
|
|
|
<div className="space-y-4 p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="p-2 bg-green-500/10 rounded-lg border border-green-500/20">
|
|
|
|
|
<MapPin className="w-4 h-4 text-green-500" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="font-medium text-[var(--text-primary)]">Ubicaciones</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-bold text-[var(--text-primary)]">
|
|
|
|
|
{usageSummary.usage.locations.current}<span className="text-[var(--text-tertiary)]">/</span>
|
|
|
|
|
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<ProgressBar value={usageSummary.usage.locations.usage_percentage} />
|
|
|
|
|
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
|
|
|
|
<span>{usageSummary.usage.locations.usage_percentage}% utilizado</span>
|
|
|
|
|
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : `${usageSummary.usage.locations.limit - usageSummary.usage.locations.current} restantes`}</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Products */}
|
|
|
|
|
<div className="space-y-4 p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="p-2 bg-purple-500/10 rounded-lg border border-purple-500/20">
|
|
|
|
|
<Package className="w-4 h-4 text-purple-500" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="font-medium text-[var(--text-primary)]">Productos</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-bold text-[var(--text-primary)]">
|
|
|
|
|
{usageSummary.usage.products.current}<span className="text-[var(--text-tertiary)]">/</span>
|
|
|
|
|
<span className="text-[var(--text-tertiary)]">{usageSummary.usage.products.unlimited ? '∞' : usageSummary.usage.products.limit}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<ProgressBar value={usageSummary.usage.products.usage_percentage} />
|
|
|
|
|
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
|
|
|
|
<span>{usageSummary.usage.products.usage_percentage}% utilizado</span>
|
|
|
|
|
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : 'Ilimitado'}</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Available Plans */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold mb-6 text-[var(--text-primary)] flex items-center">
|
|
|
|
|
<Crown className="w-5 h-5 mr-2 text-yellow-500" />
|
|
|
|
|
Planes Disponibles
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
|
{Object.entries(availablePlans.plans).map(([planKey, plan]) => {
|
|
|
|
|
const isCurrentPlan = usageSummary.plan === planKey;
|
|
|
|
|
const getPlanColor = () => {
|
|
|
|
|
switch (planKey) {
|
|
|
|
|
case 'starter': return 'border-blue-500/30 bg-blue-500/5';
|
|
|
|
|
case 'professional': return 'border-purple-500/30 bg-purple-500/5';
|
|
|
|
|
case 'enterprise': return 'border-amber-500/30 bg-amber-500/5';
|
|
|
|
|
default: return 'border-[var(--border-primary)] bg-[var(--bg-secondary)]';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card
|
|
|
|
|
key={planKey}
|
|
|
|
|
className={`relative p-6 ${getPlanColor()} ${
|
|
|
|
|
isCurrentPlan ? 'ring-2 ring-[var(--color-primary)]' : ''
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{plan.popular && (
|
|
|
|
|
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
|
|
|
|
<Badge variant="primary" className="px-3 py-1">
|
|
|
|
|
<Star className="w-3 h-3 mr-1" />
|
|
|
|
|
Más Popular
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="text-center mb-6">
|
|
|
|
|
<h4 className="text-xl font-bold text-[var(--text-primary)] mb-2">{plan.name}</h4>
|
|
|
|
|
<div className="text-3xl font-bold text-[var(--color-primary)] mb-1">
|
|
|
|
|
{subscriptionService.formatPrice(plan.monthly_price)}
|
|
|
|
|
<span className="text-lg text-[var(--text-secondary)]">/mes</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">{plan.description}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3 mb-6">
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
<Users className="w-4 h-4 text-[var(--color-primary)]" />
|
|
|
|
|
<span>{plan.max_users === -1 ? 'Usuarios ilimitados' : `${plan.max_users} usuarios`}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
<MapPin className="w-4 h-4 text-[var(--color-primary)]" />
|
|
|
|
|
<span>{plan.max_locations === -1 ? 'Ubicaciones ilimitadas' : `${plan.max_locations} ubicación${plan.max_locations > 1 ? 'es' : ''}`}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
<Package className="w-4 h-4 text-[var(--color-primary)]" />
|
|
|
|
|
<span>{plan.max_products === -1 ? 'Productos ilimitados' : `${plan.max_products} productos`}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-21 13:27:50 +02:00
|
|
|
{/* Features Section */}
|
|
|
|
|
<div className="border-t border-[var(--border-color)] pt-4 mb-6">
|
|
|
|
|
<h5 className="text-sm font-semibold text-[var(--text-primary)] mb-3 flex items-center">
|
|
|
|
|
<TrendingUp className="w-4 h-4 mr-2 text-[var(--color-primary)]" />
|
|
|
|
|
Funcionalidades Incluidas
|
|
|
|
|
</h5>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{(() => {
|
|
|
|
|
const getPlanFeatures = (planKey: string) => {
|
|
|
|
|
switch (planKey) {
|
|
|
|
|
case 'starter':
|
|
|
|
|
return [
|
|
|
|
|
'✓ Panel de Control Básico',
|
|
|
|
|
'✓ Gestión de Inventario',
|
|
|
|
|
'✓ Gestión de Pedidos',
|
|
|
|
|
'✓ Gestión de Proveedores',
|
|
|
|
|
'✓ Punto de Venta Básico',
|
|
|
|
|
'✗ Analytics Avanzados',
|
|
|
|
|
'✗ Pronósticos IA',
|
|
|
|
|
'✗ Insights Predictivos'
|
|
|
|
|
];
|
|
|
|
|
case 'professional':
|
|
|
|
|
return [
|
|
|
|
|
'✓ Panel de Control Avanzado',
|
|
|
|
|
'✓ Gestión de Inventario Completa',
|
|
|
|
|
'✓ Analytics de Ventas',
|
|
|
|
|
'✓ Pronósticos con IA (92% precisión)',
|
|
|
|
|
'✓ Análisis de Rendimiento',
|
|
|
|
|
'✓ Optimización de Producción',
|
|
|
|
|
'✓ Integración POS',
|
|
|
|
|
'✗ Insights Predictivos Avanzados'
|
|
|
|
|
];
|
|
|
|
|
case 'enterprise':
|
|
|
|
|
return [
|
|
|
|
|
'✓ Todas las funcionalidades Professional',
|
|
|
|
|
'✓ Insights Predictivos con IA',
|
|
|
|
|
'✓ Analytics Multi-ubicación',
|
|
|
|
|
'✓ Integración ERP',
|
|
|
|
|
'✓ API Personalizada',
|
|
|
|
|
'✓ Gestor de Cuenta Dedicado',
|
|
|
|
|
'✓ Soporte 24/7 Prioritario',
|
|
|
|
|
'✓ Demo Personalizada'
|
|
|
|
|
];
|
|
|
|
|
default:
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return getPlanFeatures(planKey).map((feature, index) => (
|
|
|
|
|
<div key={index} className={`text-xs flex items-center gap-2 ${
|
|
|
|
|
feature.startsWith('✓')
|
|
|
|
|
? 'text-green-600'
|
|
|
|
|
: 'text-[var(--text-secondary)] opacity-60'
|
|
|
|
|
}`}>
|
|
|
|
|
<span>{feature}</span>
|
|
|
|
|
</div>
|
|
|
|
|
));
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-20 08:59:12 +02:00
|
|
|
{isCurrentPlan ? (
|
|
|
|
|
<Badge variant="success" className="w-full justify-center py-2">
|
|
|
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
|
|
|
Plan Actual
|
|
|
|
|
</Badge>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
variant={plan.popular ? 'primary' : 'outline'}
|
|
|
|
|
className="w-full"
|
|
|
|
|
onClick={() => handleUpgradeClick(planKey)}
|
|
|
|
|
>
|
|
|
|
|
{plan.contact_sales ? 'Contactar Ventas' : 'Cambiar Plan'}
|
|
|
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Upgrade Modal */}
|
|
|
|
|
{upgradeDialogOpen && selectedPlan && availablePlans && (
|
|
|
|
|
<Modal
|
|
|
|
|
isOpen={upgradeDialogOpen}
|
|
|
|
|
onClose={() => setUpgradeDialogOpen(false)}
|
|
|
|
|
title="Confirmar Cambio de Plan"
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<p className="text-[var(--text-secondary)]">
|
|
|
|
|
¿Estás seguro de que quieres cambiar tu plan de suscripción?
|
|
|
|
|
</p>
|
|
|
|
|
{availablePlans.plans[selectedPlan] && usageSummary && (
|
|
|
|
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg space-y-2">
|
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
<span>Plan actual:</span>
|
|
|
|
|
<span>{subscriptionService.getPlanDisplayInfo(usageSummary.plan).name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
<span>Nuevo plan:</span>
|
|
|
|
|
<span>{availablePlans.plans[selectedPlan].name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-between font-medium">
|
|
|
|
|
<span>Nuevo precio:</span>
|
|
|
|
|
<span>{subscriptionService.formatPrice(availablePlans.plans[selectedPlan].monthly_price)}/mes</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2 pt-4">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setUpgradeDialogOpen(false)}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
|
|
|
|
Cancelar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={handleUpgradeConfirm}
|
|
|
|
|
disabled={upgrading}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
|
|
|
|
{upgrading ? 'Procesando...' : 'Confirmar Cambio'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
2025-08-28 23:40:44 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ProfilePage;
|