Add subcription level filtering

This commit is contained in:
Urtzi Alfaro
2025-09-21 13:27:50 +02:00
parent 29065f5337
commit e1b3184413
21 changed files with 1137 additions and 122 deletions

View File

@@ -0,0 +1,60 @@
/**
* GlobalSubscriptionHandler - Listens for subscription errors and shows upgrade dialogs
*/
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { subscriptionErrorEmitter, SubscriptionError } from '../../api/client/apiClient';
import SubscriptionErrorHandler from './SubscriptionErrorHandler';
const GlobalSubscriptionHandler: React.FC = () => {
const [subscriptionError, setSubscriptionError] = useState<SubscriptionError | null>(null);
const [showErrorDialog, setShowErrorDialog] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const handleSubscriptionError = (event: CustomEvent<SubscriptionError>) => {
setSubscriptionError(event.detail);
setShowErrorDialog(true);
};
subscriptionErrorEmitter.addEventListener(
'subscriptionError',
handleSubscriptionError as EventListener
);
return () => {
subscriptionErrorEmitter.removeEventListener(
'subscriptionError',
handleSubscriptionError as EventListener
);
};
}, []);
const handleClose = () => {
setShowErrorDialog(false);
setSubscriptionError(null);
};
const handleUpgrade = () => {
setShowErrorDialog(false);
setSubscriptionError(null);
navigate('/app/settings/profile');
};
if (!subscriptionError) {
return null;
}
return (
<SubscriptionErrorHandler
error={subscriptionError}
isOpen={showErrorDialog}
onClose={handleClose}
onUpgrade={handleUpgrade}
/>
);
};
export default GlobalSubscriptionHandler;
export { GlobalSubscriptionHandler };

View File

@@ -0,0 +1,157 @@
/**
* SubscriptionErrorHandler - Handles subscription-related API errors
*/
import React from 'react';
import { Modal, Button, Card } from '../ui';
import { Crown, Lock, ArrowRight, AlertTriangle } from 'lucide-react';
interface SubscriptionError {
error: string;
message: string;
code: string;
details: {
required_feature: string;
required_level: string;
current_plan: string;
upgrade_url: string;
};
}
interface SubscriptionErrorHandlerProps {
error: SubscriptionError;
isOpen: boolean;
onClose: () => void;
onUpgrade: () => void;
}
const SubscriptionErrorHandler: React.FC<SubscriptionErrorHandlerProps> = ({
error,
isOpen,
onClose,
onUpgrade
}) => {
const getFeatureDisplayName = (feature: string) => {
const featureNames: Record<string, string> = {
analytics: 'Analytics',
forecasting: 'Pronósticos',
ai_insights: 'Insights de IA',
production_optimization: 'Optimización de Producción',
multi_location: 'Multi-ubicación'
};
return featureNames[feature] || feature;
};
const getLevelDisplayName = (level: string) => {
const levelNames: Record<string, string> = {
basic: 'Básico',
advanced: 'Avanzado',
predictive: 'Predictivo'
};
return levelNames[level] || level;
};
const getRequiredPlan = (level: string) => {
switch (level) {
case 'advanced':
return 'Professional';
case 'predictive':
return 'Enterprise';
default:
return 'Professional';
}
};
const getPlanColor = (plan: string) => {
switch (plan.toLowerCase()) {
case 'professional':
return 'bg-gradient-to-br from-purple-500 to-indigo-600';
case 'enterprise':
return 'bg-gradient-to-br from-yellow-400 to-orange-500';
default:
return 'bg-gradient-to-br from-blue-500 to-cyan-600';
}
};
const requiredPlan = getRequiredPlan(error.details.required_level);
const featureName = getFeatureDisplayName(error.details.required_feature);
const levelName = getLevelDisplayName(error.details.required_level);
return (
<Modal isOpen={isOpen} onClose={onClose} size="md">
<div className="p-6">
<div className="text-center mb-6">
<div className={`w-16 h-16 ${getPlanColor(requiredPlan)} rounded-full flex items-center justify-center mx-auto mb-4`}>
<Crown className="w-8 h-8 text-white" />
</div>
<h2 className="text-xl font-bold text-[var(--text-primary)] mb-2">
Actualización de Plan Requerida
</h2>
<p className="text-[var(--text-secondary)]">
Esta funcionalidad requiere un plan {requiredPlan} o superior
</p>
</div>
<Card className="bg-[var(--bg-secondary)] border-0 p-4 mb-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Funcionalidad:</span>
<span className="font-medium text-[var(--text-primary)]">{featureName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Nivel requerido:</span>
<span className="font-medium text-[var(--text-primary)]">{levelName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Plan actual:</span>
<span className="font-medium text-[var(--text-primary)] capitalize">{error.details.current_plan}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-secondary)]">Plan requerido:</span>
<span className="font-medium text-[var(--color-primary)]">{requiredPlan}</span>
</div>
</div>
</Card>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm text-amber-800 font-medium mb-1">
Acceso Restringido
</p>
<p className="text-xs text-amber-700">
{error.message}
</p>
</div>
</div>
</div>
<div className="space-y-3">
<Button
onClick={onUpgrade}
className="w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-dark)] text-white font-medium py-2 px-4 rounded-lg transition-colors flex items-center justify-center gap-2"
>
Ver Planes y Actualizar
<ArrowRight className="w-4 h-4" />
</Button>
<Button
onClick={onClose}
variant="outline"
className="w-full text-[var(--text-secondary)] py-2 px-4 rounded-lg border border-[var(--border-color)] hover:bg-[var(--bg-secondary)] transition-colors"
>
Entendido
</Button>
</div>
<div className="mt-6 text-center text-xs text-[var(--text-secondary)] flex items-center justify-center gap-1">
<Lock className="w-3 h-3" />
Funcionalidad protegida por suscripción
</div>
</div>
</Modal>
);
};
export default SubscriptionErrorHandler;

View File

@@ -4,6 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { useAuthUser, useIsAuthenticated } from '../../../stores';
import { useCurrentTenantAccess } from '../../../stores/tenant.store';
import { getNavigationRoutes, canAccessRoute, ROUTES } from '../../../router/routes.config';
import { useSubscriptionAwareRoutes } from '../../../hooks/useSubscriptionAwareRoutes';
import { Button } from '../../ui';
import { Badge } from '../../ui';
import { Tooltip } from '../../ui';
@@ -136,11 +137,13 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const sidebarRef = React.useRef<HTMLDivElement>(null);
// Get navigation routes from config and convert to navigation items - memoized
// Get subscription-aware navigation routes
const baseNavigationRoutes = useMemo(() => getNavigationRoutes(), []);
const { filteredRoutes: subscriptionFilteredRoutes } = useSubscriptionAwareRoutes(baseNavigationRoutes);
// Convert routes to navigation items - memoized
const navigationItems = useMemo(() => {
const navigationRoutes = getNavigationRoutes();
const convertRoutesToItems = (routes: typeof navigationRoutes): NavigationItem[] => {
const convertRoutesToItems = (routes: typeof subscriptionFilteredRoutes): NavigationItem[] => {
return routes.map(route => ({
id: route.path,
label: route.title,
@@ -152,8 +155,8 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
}));
};
return customItems || convertRoutesToItems(navigationRoutes);
}, [customItems]);
return customItems || convertRoutesToItems(subscriptionFilteredRoutes);
}, [customItems, subscriptionFilteredRoutes]);
// Filter items based on user permissions - memoized to prevent infinite re-renders
const visibleItems = useMemo(() => {