Files
smoothschedule/frontend/src/hooks/usePlatform.ts
poduck b384d9912a Add TenantCustomTier system and fix BusinessEditModal feature loading
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>
2025-12-12 21:00:54 -05:00

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