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,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
};
};