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

@@ -9,6 +9,7 @@ import { AppRouter } from './router/AppRouter';
import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider } from './contexts/AuthContext';
import { SSEProvider } from './contexts/SSEContext';
import GlobalSubscriptionHandler from './components/auth/GlobalSubscriptionHandler';
import './i18n';
const queryClient = new QueryClient({
@@ -37,6 +38,7 @@ function App() {
<SSEProvider>
<Suspense fallback={<LoadingSpinner overlay />}>
<AppRouter />
<GlobalSubscriptionHandler />
<Toaster
position="top-right"
toastOptions={{

View File

@@ -18,6 +18,27 @@ export interface ApiError {
details?: any;
}
export interface SubscriptionError {
error: string;
message: string;
code: string;
details: {
required_feature: string;
required_level: string;
current_plan: string;
upgrade_url: string;
};
}
// Subscription error event emitter
class SubscriptionErrorEmitter extends EventTarget {
emitSubscriptionError(error: SubscriptionError) {
this.dispatchEvent(new CustomEvent('subscriptionError', { detail: error }));
}
}
export const subscriptionErrorEmitter = new SubscriptionErrorEmitter();
class ApiClient {
private client: AxiosInstance;
private baseURL: string;
@@ -146,6 +167,13 @@ class ApiClient {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
// Check for subscription errors
if (status === 403 && (data as any)?.code === 'SUBSCRIPTION_UPGRADE_REQUIRED') {
const subscriptionError = data as SubscriptionError;
subscriptionErrorEmitter.emitSubscriptionError(subscriptionError);
}
return {
message: (data as any)?.detail || (data as any)?.message || `Request failed with status ${status}`,
status,

View File

@@ -4,9 +4,7 @@
import { apiClient } from '../client';
import {
SubscriptionLimits,
FeatureCheckRequest,
FeatureCheckResponse,
UsageCheckRequest,
UsageCheckResponse,
UsageSummary,
AvailablePlans,
@@ -68,45 +66,35 @@ export class SubscriptionService {
}
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
try {
return await apiClient.get<UsageSummary>(`${this.baseUrl}/${tenantId}/summary`);
} catch (error) {
// Return mock data if backend endpoint doesn't exist yet
console.warn('Using mock subscription data - backend endpoint not implemented yet');
return this.getMockUsageSummary();
}
return apiClient.get<UsageSummary>(`${this.baseUrl}/${tenantId}/usage`);
}
async getAvailablePlans(): Promise<AvailablePlans> {
try {
return await apiClient.get<AvailablePlans>(`${this.baseUrl}/plans`);
} catch (error) {
// Return mock data if backend endpoint doesn't exist yet
console.warn('Using mock plans data - backend endpoint not implemented yet');
return this.getMockAvailablePlans();
}
return apiClient.get<AvailablePlans>('/subscriptions/plans');
}
async validatePlanUpgrade(tenantId: string, planKey: string): Promise<PlanUpgradeValidation> {
try {
return await apiClient.post<PlanUpgradeValidation>(`${this.baseUrl}/${tenantId}/validate-upgrade`, {
plan: planKey
});
} catch (error) {
console.warn('Using mock validation - backend endpoint not implemented yet');
return { can_upgrade: true };
}
return apiClient.get<PlanUpgradeValidation>(`${this.baseUrl}/${tenantId}/validate-upgrade/${planKey}`);
}
async upgradePlan(tenantId: string, planKey: string): Promise<PlanUpgradeResult> {
try {
return await apiClient.post<PlanUpgradeResult>(`${this.baseUrl}/${tenantId}/upgrade`, {
plan: planKey
});
} catch (error) {
console.warn('Using mock upgrade - backend endpoint not implemented yet');
return { success: true, message: 'Plan actualizado correctamente (modo demo)' };
}
return apiClient.post<PlanUpgradeResult>(`${this.baseUrl}/${tenantId}/upgrade?new_plan=${planKey}`, {});
}
async canAddLocation(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
return apiClient.get(`${this.baseUrl}/${tenantId}/can-add-location`);
}
async canAddProduct(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
return apiClient.get(`${this.baseUrl}/${tenantId}/can-add-product`);
}
async canAddUser(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
return apiClient.get(`${this.baseUrl}/${tenantId}/can-add-user`);
}
async hasFeature(tenantId: string, featureName: string): Promise<{ has_feature: boolean; feature_value?: any; plan?: string; reason?: string }> {
return apiClient.get(`${this.baseUrl}/${tenantId}/features/${featureName}`);
}
formatPrice(amount: number): string {
@@ -126,69 +114,6 @@ export class SubscriptionService {
};
return planInfo[planKey as keyof typeof planInfo] || { name: 'Desconocido', color: 'gray' };
}
private getMockUsageSummary(): UsageSummary {
return {
plan: 'professional',
status: 'active',
monthly_price: 49.99,
next_billing_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
usage: {
users: {
current: 3,
limit: 10,
unlimited: false,
usage_percentage: 30
},
locations: {
current: 1,
limit: 3,
unlimited: false,
usage_percentage: 33
},
products: {
current: 45,
limit: -1,
unlimited: true,
usage_percentage: 0
}
}
};
}
private getMockAvailablePlans(): AvailablePlans {
return {
plans: {
starter: {
name: 'Starter',
description: 'Perfecto para panaderías pequeñas',
monthly_price: 29.99,
max_users: 3,
max_locations: 1,
max_products: 50,
popular: false
},
professional: {
name: 'Professional',
description: 'Para panaderías en crecimiento',
monthly_price: 49.99,
max_users: 10,
max_locations: 3,
max_products: -1,
popular: true
},
enterprise: {
name: 'Enterprise',
description: 'Para grandes operaciones',
monthly_price: 99.99,
max_users: -1,
max_locations: -1,
max_products: -1,
contact_sales: true
}
}
};
}
}
export const subscriptionService = new SubscriptionService();

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(() => {

View File

@@ -0,0 +1,179 @@
/**
* Subscription hook for checking plan features and limits
*/
import { useState, useEffect, useCallback } from 'react';
import { subscriptionService } from '../api';
import { useCurrentTenant } from '../stores';
import { useAuthUser } from '../stores/auth.store';
export interface SubscriptionFeature {
hasFeature: boolean;
featureLevel?: string;
reason?: string;
}
export interface SubscriptionLimits {
canAddUser: boolean;
canAddLocation: boolean;
canAddProduct: boolean;
usageData?: any;
}
export interface SubscriptionInfo {
plan: string;
status: 'active' | 'inactive' | 'past_due' | 'cancelled';
features: Record<string, any>;
loading: boolean;
error?: string;
}
export const useSubscription = () => {
const [subscriptionInfo, setSubscriptionInfo] = useState<SubscriptionInfo>({
plan: 'starter',
status: 'active',
features: {},
loading: true,
});
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id;
// Load subscription data
const loadSubscriptionData = useCallback(async () => {
if (!tenantId) {
setSubscriptionInfo(prev => ({ ...prev, loading: false, error: 'No tenant ID available' }));
return;
}
try {
setSubscriptionInfo(prev => ({ ...prev, loading: true, error: undefined }));
const usageSummary = await subscriptionService.getUsageSummary(tenantId);
setSubscriptionInfo({
plan: usageSummary.plan,
status: usageSummary.status,
features: usageSummary.usage || {},
loading: false,
});
} catch (error) {
console.error('Error loading subscription data:', error);
setSubscriptionInfo(prev => ({
...prev,
loading: false,
error: 'Failed to load subscription data'
}));
}
}, [tenantId]);
useEffect(() => {
loadSubscriptionData();
}, [loadSubscriptionData]);
// Check if user has a specific feature
const hasFeature = useCallback(async (featureName: string): Promise<SubscriptionFeature> => {
if (!tenantId) {
return { hasFeature: false, reason: 'No tenant ID available' };
}
try {
const result = await subscriptionService.hasFeature(tenantId, featureName);
return {
hasFeature: result.has_feature,
featureLevel: result.feature_value,
reason: result.reason
};
} catch (error) {
console.error('Error checking feature:', error);
return { hasFeature: false, reason: 'Error checking feature access' };
}
}, [tenantId]);
// Check analytics access level
const getAnalyticsAccess = useCallback((): { hasAccess: boolean; level: string; reason?: string } => {
const { plan } = subscriptionInfo;
switch (plan) {
case 'starter':
return { hasAccess: true, level: 'basic' };
case 'professional':
return { hasAccess: true, level: 'advanced' };
case 'enterprise':
return { hasAccess: true, level: 'predictive' };
default:
return { hasAccess: false, level: 'none', reason: 'No valid subscription plan' };
}
}, [subscriptionInfo.plan]);
// Check if user can access specific analytics features
const canAccessAnalytics = useCallback((requiredLevel: 'basic' | 'advanced' | 'predictive' = 'basic'): boolean => {
const { hasAccess, level } = getAnalyticsAccess();
if (!hasAccess) return false;
const levelHierarchy = {
'basic': 1,
'advanced': 2,
'predictive': 3
};
return levelHierarchy[level as keyof typeof levelHierarchy] >= levelHierarchy[requiredLevel];
}, [getAnalyticsAccess]);
// Check if user can access forecasting features
const canAccessForecasting = useCallback((): boolean => {
return canAccessAnalytics('advanced'); // Forecasting requires advanced or higher
}, [canAccessAnalytics]);
// Check if user can access AI insights
const canAccessAIInsights = useCallback((): boolean => {
return canAccessAnalytics('predictive'); // AI Insights requires enterprise plan
}, [canAccessAnalytics]);
// Check usage limits
const checkLimits = useCallback(async (): Promise<SubscriptionLimits> => {
if (!tenantId) {
return {
canAddUser: false,
canAddLocation: false,
canAddProduct: false
};
}
try {
const [userCheck, locationCheck, productCheck] = await Promise.all([
subscriptionService.canAddUser(tenantId),
subscriptionService.canAddLocation(tenantId),
subscriptionService.canAddProduct(tenantId)
]);
return {
canAddUser: userCheck.can_add,
canAddLocation: locationCheck.can_add,
canAddProduct: productCheck.can_add,
};
} catch (error) {
console.error('Error checking limits:', error);
return {
canAddUser: false,
canAddLocation: false,
canAddProduct: false
};
}
}, [tenantId]);
return {
subscriptionInfo,
hasFeature,
getAnalyticsAccess,
canAccessAnalytics,
canAccessForecasting,
canAccessAIInsights,
checkLimits,
refreshSubscription: loadSubscriptionData,
};
};
export default useSubscription;

View File

@@ -0,0 +1,69 @@
/**
* Hook for filtering routes based on subscription features
*/
import { useMemo } from 'react';
import { RouteConfig } from '../router/routes.config';
import { useSubscription } from './useSubscription';
export const useSubscriptionAwareRoutes = (routes: RouteConfig[]) => {
const { subscriptionInfo, canAccessAnalytics } = useSubscription();
const filteredRoutes = useMemo(() => {
const filterRoutesBySubscription = (routeList: RouteConfig[]): RouteConfig[] => {
return routeList.reduce((filtered, route) => {
// Check if route requires subscription features
if (route.requiredAnalyticsLevel) {
const hasAccess = canAccessAnalytics(route.requiredAnalyticsLevel);
if (!hasAccess) {
return filtered; // Skip this route
}
}
// Handle specific analytics routes
if (route.path === '/app/analytics') {
// Only show analytics if user has at least basic access
if (!canAccessAnalytics('basic')) {
return filtered; // Skip analytics entirely
}
}
// Filter children recursively
const filteredRoute = {
...route,
children: route.children ? filterRoutesBySubscription(route.children) : route.children
};
// Only include parent if it has accessible children or is accessible itself
if (route.children) {
if (filteredRoute.children && filteredRoute.children.length > 0) {
filtered.push(filteredRoute);
} else if (!route.requiredAnalyticsLevel) {
// Include parent without children if it doesn't require subscription
filtered.push({ ...route, children: [] });
}
} else {
filtered.push(filteredRoute);
}
return filtered;
}, [] as RouteConfig[]);
};
if (subscriptionInfo.loading) {
// While loading, show basic routes only
return routes.filter(route =>
!route.requiredAnalyticsLevel &&
route.path !== '/app/analytics'
);
}
return filterRoutesBySubscription(routes);
}, [routes, subscriptionInfo, canAccessAnalytics]);
return {
filteredRoutes,
subscriptionInfo,
isLoading: subscriptionInfo.loading
};
};

View File

@@ -1209,6 +1209,67 @@ const ProfilePage: React.FC = () => {
</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" />

View File

@@ -14,6 +14,8 @@ export interface RouteConfig {
requiresAuth: boolean;
requiredRoles?: string[];
requiredPermissions?: string[];
requiredSubscriptionFeature?: string;
requiredAnalyticsLevel?: 'basic' | 'advanced' | 'predictive';
showInNavigation?: boolean;
showInBreadcrumbs?: boolean;
children?: RouteConfig[];
@@ -343,7 +345,7 @@ export const routesConfig: RouteConfig[] = [
],
},
// Analytics Section
// Analytics Section - Subscription protected
{
path: '/app/analytics',
name: 'Analytics',
@@ -352,6 +354,7 @@ export const routesConfig: RouteConfig[] = [
icon: 'sales',
requiresAuth: true,
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
requiredAnalyticsLevel: 'basic',
showInNavigation: true,
children: [
{
@@ -362,6 +365,7 @@ export const routesConfig: RouteConfig[] = [
icon: 'forecasting',
requiresAuth: true,
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
requiredAnalyticsLevel: 'advanced',
showInNavigation: true,
showInBreadcrumbs: true,
},
@@ -373,6 +377,7 @@ export const routesConfig: RouteConfig[] = [
icon: 'sales',
requiresAuth: true,
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
requiredAnalyticsLevel: 'advanced',
showInNavigation: true,
showInBreadcrumbs: true,
},
@@ -384,6 +389,7 @@ export const routesConfig: RouteConfig[] = [
icon: 'sales',
requiresAuth: true,
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
requiredAnalyticsLevel: 'advanced',
showInNavigation: true,
showInBreadcrumbs: true,
},
@@ -395,6 +401,7 @@ export const routesConfig: RouteConfig[] = [
icon: 'forecasting',
requiresAuth: true,
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
requiredAnalyticsLevel: 'predictive',
showInNavigation: true,
showInBreadcrumbs: true,
},