Add role-based filtering and imporve code
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, Button, Badge } from '../../ui';
|
||||
import { CheckCircle, Users, MapPin, Package, TrendingUp, Star, ArrowRight } from 'lucide-react';
|
||||
import { subscriptionService, type AvailablePlans } from '../../../api';
|
||||
import { CheckCircle, Users, MapPin, Package, TrendingUp, Star, ArrowRight, Zap } from 'lucide-react';
|
||||
import { subscriptionService, type AvailablePlans, type PlanMetadata, SUBSCRIPTION_TIERS } from '../../../api';
|
||||
|
||||
interface SubscriptionSelectionProps {
|
||||
selectedPlan: string;
|
||||
@@ -24,14 +24,18 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const [availablePlans, setAvailablePlans] = useState<AvailablePlans | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
try {
|
||||
const plans = await subscriptionService.getAvailablePlans();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const plans = await subscriptionService.fetchAvailablePlans();
|
||||
setAvailablePlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error fetching subscription plans:', error);
|
||||
} catch (err) {
|
||||
console.error('Error fetching subscription plans:', err);
|
||||
setError('No se pudieron cargar los planes. Por favor, intenta de nuevo.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -40,7 +44,7 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
fetchPlans();
|
||||
}, []);
|
||||
|
||||
if (loading || !availablePlans) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-color-primary"></div>
|
||||
@@ -48,19 +52,107 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !availablePlans) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<div className="text-color-error text-center">
|
||||
<p className="font-semibold">{error || 'Error al cargar los planes'}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Intentar de nuevo
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleTrialToggle = () => {
|
||||
if (onTrialSelect) {
|
||||
onTrialSelect(!trialSelected);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to translate feature names to Spanish
|
||||
const translateFeature = (feature: string): string => {
|
||||
const translations: Record<string, string> = {
|
||||
'inventory_management': 'Gestión de inventario',
|
||||
'sales_tracking': 'Seguimiento de ventas',
|
||||
'basic_analytics': 'Analíticas básicas',
|
||||
'basic_forecasting': 'Pronósticos básicos',
|
||||
'pos_integration': 'Punto de venta integrado',
|
||||
'production_planning': 'Planificación de producción',
|
||||
'supplier_management': 'Gestión de proveedores',
|
||||
'recipe_management': 'Gestión de recetas',
|
||||
'advanced_analytics': 'Analíticas avanzadas',
|
||||
'ai_forecasting': 'Pronósticos con IA',
|
||||
'weather_data_integration': 'Integración datos meteorológicos',
|
||||
'multi_location': 'Multi-ubicación',
|
||||
'custom_reports': 'Reportes personalizados',
|
||||
'api_access': 'Acceso API',
|
||||
'priority_support': 'Soporte prioritario',
|
||||
'dedicated_account_manager': 'Manager de cuenta dedicado',
|
||||
'sla_guarantee': 'Garantía SLA',
|
||||
'custom_integrations': 'Integraciones personalizadas',
|
||||
'white_label': 'Marca blanca',
|
||||
'advanced_security': 'Seguridad avanzada',
|
||||
'audit_logs': 'Registros de auditoría',
|
||||
'role_based_access': 'Control de acceso basado en roles',
|
||||
'custom_workflows': 'Flujos de trabajo personalizados',
|
||||
'training_sessions': 'Sesiones de capacitación',
|
||||
'onboarding_support': 'Soporte de incorporación',
|
||||
'data_export': 'Exportación de datos',
|
||||
'backup_restore': 'Respaldo y restauración',
|
||||
'mobile_app': 'Aplicación móvil',
|
||||
'offline_mode': 'Modo offline',
|
||||
'real_time_sync': 'Sincronización en tiempo real',
|
||||
'notifications': 'Notificaciones',
|
||||
'email_alerts': 'Alertas por email',
|
||||
'sms_alerts': 'Alertas por SMS',
|
||||
'inventory_alerts': 'Alertas de inventario',
|
||||
'low_stock_alerts': 'Alertas de stock bajo',
|
||||
'expiration_tracking': 'Seguimiento de caducidad',
|
||||
'batch_tracking': 'Seguimiento de lotes',
|
||||
'quality_control': 'Control de calidad',
|
||||
'compliance_reporting': 'Reportes de cumplimiento',
|
||||
'financial_reports': 'Reportes financieros',
|
||||
'tax_reports': 'Reportes de impuestos',
|
||||
'waste_tracking': 'Seguimiento de desperdicios',
|
||||
'cost_analysis': 'Análisis de costos',
|
||||
'profit_margins': 'Márgenes de ganancia',
|
||||
'sales_forecasting': 'Pronóstico de ventas',
|
||||
'demand_planning': 'Planificación de demanda',
|
||||
'seasonal_trends': 'Tendencias estacionales',
|
||||
'customer_analytics': 'Analíticas de clientes',
|
||||
'loyalty_program': 'Programa de lealtad',
|
||||
'discount_management': 'Gestión de descuentos',
|
||||
'promotion_tracking': 'Seguimiento de promociones',
|
||||
'gift_cards': 'Tarjetas de regalo',
|
||||
'online_ordering': 'Pedidos en línea',
|
||||
'delivery_management': 'Gestión de entregas',
|
||||
'route_optimization': 'Optimización de rutas',
|
||||
'driver_tracking': 'Seguimiento de conductores',
|
||||
'customer_portal': 'Portal de clientes',
|
||||
'vendor_portal': 'Portal de proveedores',
|
||||
'invoice_management': 'Gestión de facturas',
|
||||
'payment_processing': 'Procesamiento de pagos',
|
||||
'purchase_orders': 'Órdenes de compra',
|
||||
'receiving_management': 'Gestión de recepciones'
|
||||
};
|
||||
return translations[feature] || feature.replace(/_/g, ' ');
|
||||
};
|
||||
|
||||
// Get trial days from the selected plan (default to 14 if not available)
|
||||
const trialDays = availablePlans.plans[selectedPlan]?.trial_days || 14;
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{showTrialOption && (
|
||||
<Card className="p-4 border-2 border-color-primary/30 bg-bg-primary">
|
||||
<Card className="p-4 border-2 border-color-primary/30 bg-gradient-to-r from-color-primary/5 to-color-primary/10">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="p-2.5 bg-color-primary/10 rounded-lg flex-shrink-0">
|
||||
<div className="p-2.5 bg-color-primary/20 rounded-lg flex-shrink-0">
|
||||
<Star className="w-5 h-5 text-color-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -68,7 +160,7 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
{t('auth:subscription.trial_title', 'Prueba gratuita')}
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{t('auth:subscription.trial_description', 'Obtén 3 meses de prueba gratuita como usuario piloto')}
|
||||
{t('auth:subscription.trial_description', `Obtén ${trialDays} días de prueba gratuita - sin tarjeta de crédito requerida`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,9 +170,14 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
onClick={handleTrialToggle}
|
||||
className="w-full sm:w-auto flex-shrink-0 min-w-[100px]"
|
||||
>
|
||||
{trialSelected
|
||||
? t('auth:subscription.trial_active', 'Activo')
|
||||
: t('auth:subscription.trial_activate', 'Activar')}
|
||||
{trialSelected ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>{t('auth:subscription.trial_active', 'Activo')}</span>
|
||||
</div>
|
||||
) : (
|
||||
t('auth:subscription.trial_activate', 'Activar')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -89,19 +186,20 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
<div className="space-y-3">
|
||||
{Object.entries(availablePlans.plans).map(([planKey, plan]) => {
|
||||
const isSelected = selectedPlan === planKey;
|
||||
const metadata = plan as PlanMetadata;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={planKey}
|
||||
className={`relative p-6 cursor-pointer transition-all duration-200 border-2 ${
|
||||
isSelected
|
||||
? 'border-color-primary bg-color-primary/5 shadow-lg'
|
||||
? 'border-color-primary bg-color-primary/5 shadow-lg ring-2 ring-color-primary/20'
|
||||
: 'border-border-primary bg-bg-primary hover:border-color-primary/40 hover:shadow-md'
|
||||
} ${plan.popular ? 'pt-8' : ''}`}
|
||||
} ${metadata.popular ? 'pt-8' : ''}`}
|
||||
onClick={() => onPlanSelect(planKey)}
|
||||
>
|
||||
{/* Popular Badge */}
|
||||
{plan.popular && (
|
||||
{metadata.popular && (
|
||||
<div className="absolute top-0 left-0 right-0 flex justify-center -translate-y-1/2 z-20">
|
||||
<Badge variant="primary" className="px-4 py-1.5 text-xs font-bold flex items-center gap-1.5 shadow-lg rounded-full">
|
||||
<Star className="w-3.5 h-3.5 fill-current" />
|
||||
@@ -115,14 +213,28 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
{/* Header Section: Plan Info & Pricing */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h4 className="text-2xl font-bold text-text-primary mb-2">{plan.name}</h4>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h4 className="text-2xl font-bold text-text-primary">{metadata.name}</h4>
|
||||
{metadata.trial_days > 0 && (
|
||||
<Badge variant="success" className="text-xs px-2 py-0.5">
|
||||
<Zap className="w-3 h-3 mr-1" />
|
||||
{metadata.trial_days} días gratis
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-color-primary font-semibold mb-3">{metadata.tagline}</p>
|
||||
<div className="flex items-baseline gap-1 mb-3">
|
||||
<span className="text-4xl font-bold text-color-primary">
|
||||
{subscriptionService.formatPrice(plan.monthly_price)}
|
||||
{subscriptionService.formatPrice(metadata.monthly_price)}
|
||||
</span>
|
||||
<span className="text-base text-text-secondary font-medium">/mes</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary leading-relaxed max-w-prose">{plan.description}</p>
|
||||
<p className="text-sm text-text-secondary leading-relaxed max-w-prose">{metadata.description}</p>
|
||||
{metadata.recommended_for && (
|
||||
<p className="text-xs text-text-tertiary mt-2 italic">
|
||||
💡 {metadata.recommended_for}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Button - Desktop position */}
|
||||
@@ -155,71 +267,73 @@ export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> = ({
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-4 border-t border-border-primary/50">
|
||||
{/* Plan Limits */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||
<Users className="w-4 h-4 text-color-primary flex-shrink-0" />
|
||||
<span className="font-medium">{plan.max_users === -1 ? 'Usuarios ilimitados' : `${plan.max_users} usuario${plan.max_users > 1 ? 's' : ''}`}</span>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Package className="w-5 h-5 text-color-primary flex-shrink-0" />
|
||||
<h5 className="text-base font-bold text-text-primary">
|
||||
Límites del Plan
|
||||
</h5>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||
<MapPin className="w-4 h-4 text-color-primary flex-shrink-0" />
|
||||
<span className="font-medium">{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.5 text-sm text-text-primary">
|
||||
<Package className="w-4 h-4 text-color-primary flex-shrink-0" />
|
||||
<span className="font-medium">{plan.max_products === -1 ? 'Productos ilimitados' : `${plan.max_products} producto${plan.max_products > 1 ? 's' : ''}`}</span>
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||
<Users className="w-4 h-4 text-color-accent flex-shrink-0" />
|
||||
<span className="font-medium">
|
||||
{metadata.limits.users === null ? 'Usuarios ilimitados' : `${metadata.limits.users} usuario${metadata.limits.users > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||
<MapPin className="w-4 h-4 text-color-accent flex-shrink-0" />
|
||||
<span className="font-medium">
|
||||
{metadata.limits.locations === null ? 'Ubicaciones ilimitadas' : `${metadata.limits.locations} ubicación${metadata.limits.locations > 1 ? 'es' : ''}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||
<Package className="w-4 h-4 text-color-accent flex-shrink-0" />
|
||||
<span className="font-medium">
|
||||
{metadata.limits.products === null ? 'Productos ilimitados' : `${metadata.limits.products} producto${metadata.limits.products > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
</div>
|
||||
{metadata.limits.forecasts_per_day !== null && (
|
||||
<div className="flex items-center gap-2.5 text-sm text-text-primary">
|
||||
<TrendingUp className="w-4 h-4 text-color-accent flex-shrink-0" />
|
||||
<span className="font-medium">
|
||||
{metadata.limits.forecasts_per_day} pronóstico{metadata.limits.forecasts_per_day > 1 ? 's' : ''}/día
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="space-y-3 lg:pl-6 lg:border-l border-border-primary/50">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="w-5 h-5 text-color-primary flex-shrink-0" />
|
||||
<CheckCircle className="w-5 h-5 text-color-success flex-shrink-0" />
|
||||
<h5 className="text-base font-bold text-text-primary">
|
||||
{t('auth:subscription.features', 'Funcionalidades Incluidas')}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{(() => {
|
||||
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'
|
||||
];
|
||||
case 'professional':
|
||||
return [
|
||||
'Todo lo de Starter',
|
||||
'Panel Avanzado',
|
||||
'Analytics de Ventas',
|
||||
'Pronósticos con IA',
|
||||
'Optimización de Producción'
|
||||
];
|
||||
case 'enterprise':
|
||||
return [
|
||||
'Todo lo de Professional',
|
||||
'Insights Predictivos IA',
|
||||
'Analytics Multi-ubicación',
|
||||
'Integración ERP',
|
||||
'Soporte 24/7 Prioritario',
|
||||
'API Personalizada'
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return getPlanFeatures(planKey).map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-2.5 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-color-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-text-primary leading-snug">{feature}</span>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
<div className="space-y-2.5 max-h-48 overflow-y-auto pr-2 scrollbar-thin">
|
||||
{metadata.features.slice(0, 8).map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-2.5 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-color-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-text-primary leading-snug">{translateFeature(feature)}</span>
|
||||
</div>
|
||||
))}
|
||||
{metadata.features.length > 8 && (
|
||||
<p className="text-xs text-text-tertiary italic pl-6">
|
||||
+{metadata.features.length - 8} funcionalidades más
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Support Level */}
|
||||
{metadata.support && (
|
||||
<div className="pt-3 mt-3 border-t border-border-primary/30">
|
||||
<p className="text-xs text-text-secondary">
|
||||
<span className="font-semibold">Soporte:</span> {metadata.support}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user