Add subcription level filtering
This commit is contained in:
60
frontend/src/components/auth/GlobalSubscriptionHandler.tsx
Normal file
60
frontend/src/components/auth/GlobalSubscriptionHandler.tsx
Normal 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 };
|
||||
157
frontend/src/components/auth/SubscriptionErrorHandler.tsx
Normal file
157
frontend/src/components/auth/SubscriptionErrorHandler.tsx
Normal 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;
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user