Backend Implementation: - Add TenantInvitation model with lifecycle management (PENDING/ACCEPTED/EXPIRED/CANCELLED) - Create platform admin API endpoints for invitation CRUD operations - Add public token-based endpoints for invitation retrieval and acceptance - Implement schema_context wrappers to ensure tenant operations run in public schema - Add tenant permissions: can_manage_oauth_credentials, can_accept_payments, can_use_custom_domain, can_white_label, can_api_access - Fix tenant update/create serializers to handle multi-schema environment - Add migrations for tenant permissions and invitation system Frontend Implementation: - Create TenantInviteModal with comprehensive invitation form (350 lines) - Email, business name, subscription tier configuration - Custom user/resource limits - Platform permissions toggles - Future feature flags (video conferencing, event types, calendars, 2FA, logs, data deletion, POS, mobile app) - Build TenantOnboardPage with 4-step wizard for invitation acceptance - Step 1: Account setup (email, password, name) - Step 2: Business details (name, subdomain, contact) - Step 3: Payment setup (conditional based on permissions) - Step 4: Success confirmation with redirect - Extract BusinessCreateModal and BusinessEditModal into separate components - Refactor PlatformBusinesses from 1080 lines to 220 lines (80% reduction) - Add inactive businesses dropdown section (similar to staff page pattern) - Update masquerade button styling to match Users page - Remove deprecated "Add New Tenant" functionality in favor of invitation flow - Add /tenant-onboard route for public access API Integration: - Add platform.ts API functions for tenant invitations - Create React Query hooks in usePlatform.ts for invitation management - Implement proper error handling and success states - Add TypeScript interfaces for invitation types Testing: - Verified end-to-end invitation flow from creation to acceptance - Confirmed tenant, domain, and owner user creation - Validated schema context fixes for multi-tenant environment - Tested active/inactive business filtering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
171 lines
4.4 KiB
TypeScript
171 lines
4.4 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,
|
|
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 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'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
// ============================================================================
|
|
// 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),
|
|
});
|
|
};
|