Files
bakery-ia/frontend/src/pages/app/settings/profile/ProfilePage.tsx
2025-09-25 12:14:46 +02:00

956 lines
40 KiB
TypeScript

import React, { useState } from 'react';
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';
import { PageHeader } from '../../../../components/layout';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
import { useTranslation } from 'react-i18next';
import CommunicationPreferences, { type NotificationPreferences } from './CommunicationPreferences';
interface ProfileFormData {
first_name: string;
last_name: string;
email: string;
phone: string;
language: string;
timezone: string;
avatar?: string;
}
interface PasswordData {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
const ProfilePage: React.FC = () => {
const user = useAuthUser();
const { t } = useTranslation(['settings', 'auth']);
const { addToast } = useToast();
const { data: profile, isLoading: profileLoading, error: profileError } = useAuthProfile();
const updateProfileMutation = useUpdateProfile();
const changePasswordMutation = useChangePassword();
const [isEditing, setIsEditing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showPasswordForm, setShowPasswordForm] = useState(false);
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();
const [profileData, setProfileData] = useState<ProfileFormData>({
first_name: '',
last_name: '',
email: '',
phone: '',
language: 'es',
timezone: 'Europe/Madrid'
});
// 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',
avatar: profile.avatar || ''
});
// Update notification preferences with profile data
setNotificationPreferences(prev => ({
...prev,
language: profile.language || 'es',
timezone: profile.timezone || 'Europe/Madrid'
}));
}
}, [profile]);
// Load subscription data when needed
React.useEffect(() => {
if (activeTab === 'subscription' && (currentTenant?.id || user?.tenant_id) && !usageSummary) {
loadSubscriptionData();
}
}, [activeTab, currentTenant, user?.tenant_id]);
const [passwordData, setPasswordData] = useState<PasswordData>({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [notificationPreferences, setNotificationPreferences] = useState<NotificationPreferences>({
email_enabled: true,
email_alerts: true,
email_marketing: false,
email_reports: true,
whatsapp_enabled: false,
whatsapp_alerts: false,
whatsapp_reports: false,
push_enabled: true,
push_alerts: true,
push_reports: false,
quiet_hours_start: '22:00',
quiet_hours_end: '08:00',
timezone: 'Europe/Madrid',
digest_frequency: 'daily',
max_emails_per_day: 10,
language: 'es'
});
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;
};
const handleSaveProfile = async () => {
if (!validateProfile()) return;
setIsLoading(true);
try {
await updateProfileMutation.mutateAsync(profileData);
setIsEditing(false);
addToast('Perfil actualizado correctamente', 'success');
} catch (error) {
addToast('No se pudo actualizar tu perfil', 'error');
} finally {
setIsLoading(false);
}
};
const handleChangePasswordSubmit = async () => {
if (!validatePassword()) return;
setIsLoading(true);
try {
await changePasswordMutation.mutateAsync({
current_password: passwordData.currentPassword,
new_password: passwordData.newPassword,
confirm_password: passwordData.confirmPassword
});
setShowPasswordForm(false);
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
addToast('Contraseña actualizada correctamente', 'success');
} catch (error) {
addToast('No se pudo cambiar tu contraseña', 'error');
} finally {
setIsLoading(false);
}
};
const handleInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setProfileData(prev => ({ ...prev, [field]: e.target.value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const handleSelectChange = (field: keyof ProfileFormData) => (value: string) => {
setProfileData(prev => ({ ...prev, [field]: value }));
};
const handlePasswordChange = (field: keyof PasswordData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setPasswordData(prev => ({ ...prev, [field]: e.target.value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
// Notification preferences handlers
const handleSaveNotificationPreferences = async (preferences: NotificationPreferences) => {
try {
await updateProfileMutation.mutateAsync({
language: preferences.language,
timezone: preferences.timezone,
notification_preferences: preferences
});
setNotificationPreferences(preferences);
setHasPreferencesChanges(false);
} catch (error) {
throw error; // Let the component handle the error display
}
};
const handleResetNotificationPreferences = () => {
if (profile) {
setNotificationPreferences({
email_enabled: true,
email_alerts: true,
email_marketing: false,
email_reports: true,
whatsapp_enabled: false,
whatsapp_alerts: false,
whatsapp_reports: false,
push_enabled: true,
push_alerts: true,
push_reports: false,
quiet_hours_start: '22:00',
quiet_hours_end: '08:00',
timezone: profile.timezone || 'Europe/Madrid',
digest_frequency: 'daily',
max_emails_per_day: 10,
language: profile.language || 'es'
});
setHasPreferencesChanges(false);
}
};
// Subscription handlers
const loadSubscriptionData = async () => {
const 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 () => {
const 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' }
];
return (
<div className="p-6 space-y-6">
<PageHeader
title="Mi Perfil"
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"
/>
{/* Profile Header */}
{activeTab === 'profile' && (
<Card className="p-6">
<div className="flex items-center gap-6">
<div className="relative">
<Avatar
src={profile?.avatar || undefined}
alt={profile?.full_name || `${profileData.first_name} ${profileData.last_name}` || 'Usuario'}
name={profile?.avatar ? (profile?.full_name || `${profileData.first_name} ${profileData.last_name}`) : undefined}
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>
</div>
<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>
{user?.role && (
<p className="text-sm text-text-tertiary mt-1">
{t(`global_roles.${user.role}`)}
</p>
)}
<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">{t('settings:profile.online', '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" />
{t('settings:profile.edit_profile', 'Editar Perfil')}
</Button>
)}
<Button
variant="outline"
onClick={() => setShowPasswordForm(!showPasswordForm)}
className="flex items-center gap-2"
>
<Lock className="w-4 h-4" />
{t('settings:profile.change_password', 'Cambiar Contraseña')}
</Button>
</div>
</div>
</Card>
)}
{/* Profile Form */}
{activeTab === 'profile' && (
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">{t('settings:profile.personal_info', 'Información Personal')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<Input
label={t('settings:profile.fields.first_name', '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={t('settings:profile.fields.last_name', 'Apellidos')}
value={profileData.last_name}
onChange={handleInputChange('last_name')}
error={errors.last_name}
disabled={!isEditing || isLoading}
/>
<Input
type="email"
label={t('settings:profile.fields.email', '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={t('settings:profile.fields.phone', '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>
</div>
)}
</Card>
)}
{/* Password Change Form */}
{activeTab === 'profile' && showPasswordForm && (
<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" />}
/>
<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" />}
/>
<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" />}
/>
</div>
<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"
onClick={handleChangePasswordSubmit}
isLoading={isLoading}
loadingText="Cambiando..."
>
Cambiar Contraseña
</Button>
</div>
</Card>
)}
{/* Communication Preferences Tab */}
{activeTab === 'preferences' && (
<CommunicationPreferences
userEmail={profileData.email}
userPhone={profileData.phone}
userLanguage={profileData.language}
userTimezone={profileData.timezone}
onSave={handleSaveNotificationPreferences}
onReset={handleResetNotificationPreferences}
hasChanges={hasPreferencesChanges}
/>
)}
{/* 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>
{/* 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>
{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>
)}
</div>
);
};
export default ProfilePage;