New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -102,6 +102,9 @@ class ApiClient {
// Only add auth token for non-public endpoints
if (this.authToken && !isPublicEndpoint) {
config.headers.Authorization = `Bearer ${this.authToken}`;
console.log('🔑 [API Client] Adding Authorization header for:', config.url);
} else if (!isPublicEndpoint) {
console.warn('⚠️ [API Client] No auth token available for:', config.url, 'authToken:', this.authToken ? 'exists' : 'missing');
}
// Add tenant ID only for endpoints that require it
@@ -343,7 +346,9 @@ class ApiClient {
// Configuration methods
setAuthToken(token: string | null) {
console.log('🔧 [API Client] setAuthToken called:', token ? `${token.substring(0, 20)}...` : 'null');
this.authToken = token;
console.log('✅ [API Client] authToken is now:', this.authToken ? 'set' : 'null');
}
setRefreshToken(token: string | null) {

View File

@@ -0,0 +1,89 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { enterpriseService, NetworkSummary, ChildPerformance, DistributionOverview, ForecastSummary, NetworkPerformance } from '../services/enterprise';
import { ApiError } from '../client';
// Query Keys
export const enterpriseKeys = {
all: ['enterprise'] as const,
networkSummary: (tenantId: string) => [...enterpriseKeys.all, 'network-summary', tenantId] as const,
childrenPerformance: (tenantId: string, metric: string, period: number) =>
[...enterpriseKeys.all, 'children-performance', tenantId, metric, period] as const,
distributionOverview: (tenantId: string, date?: string) =>
[...enterpriseKeys.all, 'distribution-overview', tenantId, date] as const,
forecastSummary: (tenantId: string, days: number) =>
[...enterpriseKeys.all, 'forecast-summary', tenantId, days] as const,
networkPerformance: (tenantId: string, startDate?: string, endDate?: string) =>
[...enterpriseKeys.all, 'network-performance', tenantId, startDate, endDate] as const,
} as const;
// Hooks
export const useNetworkSummary = (
tenantId: string,
options?: Omit<UseQueryOptions<NetworkSummary, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<NetworkSummary, ApiError>({
queryKey: enterpriseKeys.networkSummary(tenantId),
queryFn: () => enterpriseService.getNetworkSummary(tenantId),
enabled: !!tenantId,
staleTime: 30000, // 30 seconds
...options,
});
};
export const useChildrenPerformance = (
tenantId: string,
metric: string,
period: number,
options?: Omit<UseQueryOptions<ChildPerformance, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ChildPerformance, ApiError>({
queryKey: enterpriseKeys.childrenPerformance(tenantId, metric, period),
queryFn: () => enterpriseService.getChildrenPerformance(tenantId, metric, period),
enabled: !!tenantId,
staleTime: 60000, // 1 minute
...options,
});
};
export const useDistributionOverview = (
tenantId: string,
targetDate?: string,
options?: Omit<UseQueryOptions<DistributionOverview, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<DistributionOverview, ApiError>({
queryKey: enterpriseKeys.distributionOverview(tenantId, targetDate),
queryFn: () => enterpriseService.getDistributionOverview(tenantId, targetDate),
enabled: !!tenantId,
staleTime: 30000, // 30 seconds
...options,
});
};
export const useForecastSummary = (
tenantId: string,
daysAhead: number = 7,
options?: Omit<UseQueryOptions<ForecastSummary, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ForecastSummary, ApiError>({
queryKey: enterpriseKeys.forecastSummary(tenantId, daysAhead),
queryFn: () => enterpriseService.getForecastSummary(tenantId, daysAhead),
enabled: !!tenantId,
staleTime: 120000, // 2 minutes
...options,
});
};
export const useNetworkPerformance = (
tenantId: string,
startDate?: string,
endDate?: string,
options?: Omit<UseQueryOptions<NetworkPerformance, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<NetworkPerformance, ApiError>({
queryKey: enterpriseKeys.networkPerformance(tenantId, startDate, endDate),
queryFn: () => enterpriseService.getNetworkPerformance(tenantId, startDate, endDate),
enabled: !!tenantId,
...options,
});
};

View File

@@ -2,7 +2,7 @@
* Subscription hook for checking plan features and limits
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { subscriptionService } from '../services/subscription';
import {
SUBSCRIPTION_TIERS,
@@ -41,7 +41,7 @@ export const useSubscription = () => {
loading: true,
});
const currentTenant = useCurrentTenant();
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id;
const { notifySubscriptionChanged, subscriptionVersion } = useSubscriptionEvents();
@@ -72,7 +72,7 @@ export const useSubscription = () => {
error: 'Failed to load subscription data'
}));
}
}, [tenantId]); // Removed notifySubscriptionChanged - it's now stable from context
}, [tenantId]);
useEffect(() => {
loadSubscriptionData();
@@ -99,7 +99,7 @@ export const useSubscription = () => {
// Check analytics access level
const getAnalyticsAccess = useCallback((): { hasAccess: boolean; level: string; reason?: string } => {
const { plan } = subscriptionInfo;
const plan = subscriptionInfo.plan;
// Convert plan string to typed SubscriptionTier
let tierKey: SubscriptionTier | undefined;

View File

@@ -0,0 +1,104 @@
import { apiClient } from '../client';
export interface NetworkSummary {
parent_tenant_id: string;
total_tenants: number;
child_tenant_count: number;
total_revenue: number;
network_sales_30d: number;
active_alerts: number;
efficiency_score: number;
growth_rate: number;
production_volume_30d: number;
pending_internal_transfers_count: number;
active_shipments_count: number;
last_updated: string;
}
export interface ChildPerformance {
rankings: Array<{
tenant_id: string;
name: string;
anonymized_name: string;
metric_value: number;
rank: number;
}>;
}
export interface DistributionOverview {
route_sequences: any[];
status_counts: {
pending: number;
in_transit: number;
delivered: number;
failed: number;
[key: string]: number;
};
}
export interface ForecastSummary {
aggregated_forecasts: Record<string, any>;
days_forecast: number;
last_updated: string;
}
export interface NetworkPerformance {
metrics: Record<string, any>;
}
export class EnterpriseService {
private readonly baseUrl = '/tenants';
async getNetworkSummary(tenantId: string): Promise<NetworkSummary> {
return apiClient.get<NetworkSummary>(`${this.baseUrl}/${tenantId}/enterprise/network-summary`);
}
async getChildrenPerformance(
tenantId: string,
metric: string = 'sales',
periodDays: number = 30
): Promise<ChildPerformance> {
const queryParams = new URLSearchParams({
metric,
period_days: periodDays.toString()
});
return apiClient.get<ChildPerformance>(
`${this.baseUrl}/${tenantId}/enterprise/children-performance?${queryParams.toString()}`
);
}
async getDistributionOverview(tenantId: string, targetDate?: string): Promise<DistributionOverview> {
const queryParams = new URLSearchParams();
if (targetDate) {
queryParams.append('target_date', targetDate);
}
return apiClient.get<DistributionOverview>(
`${this.baseUrl}/${tenantId}/enterprise/distribution-overview?${queryParams.toString()}`
);
}
async getForecastSummary(tenantId: string, daysAhead: number = 7): Promise<ForecastSummary> {
const queryParams = new URLSearchParams({
days_ahead: daysAhead.toString()
});
return apiClient.get<ForecastSummary>(
`${this.baseUrl}/${tenantId}/enterprise/forecast-summary?${queryParams.toString()}`
);
}
async getNetworkPerformance(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<NetworkPerformance> {
const queryParams = new URLSearchParams();
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
return apiClient.get<NetworkPerformance>(
`${this.baseUrl}/${tenantId}/enterprise/network-performance?${queryParams.toString()}`
);
}
}
export const enterpriseService = new EnterpriseService();

View File

@@ -23,10 +23,11 @@ import {
} from '../types/subscription';
// Map plan tiers to analytics levels based on backend data
const TIER_TO_ANALYTICS_LEVEL: Record<SubscriptionTier, AnalyticsLevel> = {
const TIER_TO_ANALYTICS_LEVEL: Record<SubscriptionTier | string, AnalyticsLevel> = {
[SUBSCRIPTION_TIERS.STARTER]: ANALYTICS_LEVELS.BASIC,
[SUBSCRIPTION_TIERS.PROFESSIONAL]: ANALYTICS_LEVELS.ADVANCED,
[SUBSCRIPTION_TIERS.ENTERPRISE]: ANALYTICS_LEVELS.PREDICTIVE
[SUBSCRIPTION_TIERS.ENTERPRISE]: ANALYTICS_LEVELS.PREDICTIVE,
'demo': ANALYTICS_LEVELS.ADVANCED, // Treat demo tier same as professional for analytics access
};
// Cache for available plans