Backend: - Add TenantCustomTier model for per-tenant feature overrides - Update EntitlementService to check custom tier before plan features - Add custom_tier action on TenantViewSet (GET/PUT/DELETE) - Add Celery task for grace period management (30-day expiry) Frontend: - Add DynamicFeaturesEditor component for dynamic feature management - Fix BusinessEditModal to load features from plan defaults when no custom tier - Update limits (max_users, max_resources, etc.) to use featureValues - Remove outdated canonical feature check from FeaturePicker (removes warning icons) - Add useBillingPlans hook for accessing billing system data - Add custom tier API functions to platform.ts Features now follow consistent rules: - Load from plan defaults when no custom tier exists - Load from custom tier when one exists - Reset to plan defaults when plan changes - Save to custom tier on edit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
5.3 KiB
TypeScript
204 lines
5.3 KiB
TypeScript
/**
|
|
* Platform Hooks
|
|
* React Query hooks for platform-level operations
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
getBusinesses,
|
|
getUsers,
|
|
getBusinessUsers,
|
|
updateBusiness,
|
|
createBusiness,
|
|
deleteBusiness,
|
|
changeBusinessPlan,
|
|
PlatformBusinessUpdate,
|
|
PlatformBusinessCreate,
|
|
getTenantInvitations,
|
|
createTenantInvitation,
|
|
resendTenantInvitation,
|
|
cancelTenantInvitation,
|
|
getInvitationByToken,
|
|
acceptInvitation,
|
|
TenantInvitationCreate,
|
|
TenantInvitationAccept
|
|
} from '../api/platform';
|
|
|
|
/**
|
|
* Hook to get all businesses (platform admin only)
|
|
*/
|
|
export const useBusinesses = () => {
|
|
return useQuery({
|
|
queryKey: ['platform', 'businesses'],
|
|
queryFn: getBusinesses,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to get all users (platform admin only)
|
|
*/
|
|
export const usePlatformUsers = () => {
|
|
return useQuery({
|
|
queryKey: ['platform', 'users'],
|
|
queryFn: getUsers,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to get users for a specific business
|
|
*/
|
|
export const useBusinessUsers = (businessId: number | null) => {
|
|
return useQuery({
|
|
queryKey: ['platform', 'business-users', businessId],
|
|
queryFn: () => getBusinessUsers(businessId!),
|
|
enabled: !!businessId,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to update a business (platform admin only)
|
|
*/
|
|
export const useUpdateBusiness = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ businessId, data }: { businessId: number; data: PlatformBusinessUpdate }) =>
|
|
updateBusiness(businessId, data),
|
|
onSuccess: () => {
|
|
// Invalidate and refetch businesses list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to change a business's subscription plan (platform admin only)
|
|
*/
|
|
export const useChangeBusinessPlan = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ businessId, planCode }: { businessId: number; planCode: string }) =>
|
|
changeBusinessPlan(businessId, planCode),
|
|
onSuccess: () => {
|
|
// Invalidate and refetch businesses list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to create a new business (platform admin only)
|
|
*/
|
|
export const useCreateBusiness = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: PlatformBusinessCreate) => createBusiness(data),
|
|
onSuccess: () => {
|
|
// Invalidate and refetch businesses list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to delete a business/tenant (platform admin only)
|
|
*/
|
|
export const useDeleteBusiness = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (businessId: number) => deleteBusiness(businessId),
|
|
onSuccess: () => {
|
|
// Invalidate and refetch businesses list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
// ============================================================================
|
|
// Tenant Invitation Hooks
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Hook to get all tenant invitations (platform admin only)
|
|
*/
|
|
export const useTenantInvitations = () => {
|
|
return useQuery({
|
|
queryKey: ['platform', 'tenant-invitations'],
|
|
queryFn: getTenantInvitations,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to create a tenant invitation (platform admin only)
|
|
*/
|
|
export const useCreateTenantInvitation = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: TenantInvitationCreate) => createTenantInvitation(data),
|
|
onSuccess: () => {
|
|
// Invalidate invitations list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'tenant-invitations'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to resend a tenant invitation (platform admin only)
|
|
*/
|
|
export const useResendTenantInvitation = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (invitationId: number) => resendTenantInvitation(invitationId),
|
|
onSuccess: () => {
|
|
// Invalidate invitations list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'tenant-invitations'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to cancel a tenant invitation (platform admin only)
|
|
*/
|
|
export const useCancelTenantInvitation = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (invitationId: number) => cancelTenantInvitation(invitationId),
|
|
onSuccess: () => {
|
|
// Invalidate invitations list
|
|
queryClient.invalidateQueries({ queryKey: ['platform', 'tenant-invitations'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to get invitation details by token (public, no auth required)
|
|
*/
|
|
export const useInvitationByToken = (token: string | null) => {
|
|
return useQuery({
|
|
queryKey: ['tenant-invitation', token],
|
|
queryFn: () => getInvitationByToken(token!),
|
|
enabled: !!token,
|
|
retry: false, // Don't retry on 404/expired invitations
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to accept an invitation (public, no auth required)
|
|
*/
|
|
export const useAcceptInvitation = () => {
|
|
return useMutation({
|
|
mutationFn: ({ token, data }: { token: string; data: TenantInvitationAccept }) =>
|
|
acceptInvitation(token, data),
|
|
});
|
|
};
|