feat: Reorganize settings sidebar and add plan-based feature locking

- Add locked state to Plugins sidebar item with plan feature check
- Create Branding section in settings with Appearance, Email Templates, Custom Domains
- Split Domains page into Booking (URLs, redirects) and Custom Domains (BYOD, purchase)
- Add booking_return_url field to Tenant model for customer redirects
- Update SidebarItem component to support locked prop with lock icon
- Move Email Templates from main sidebar to Settings > Branding
- Add communication credits hooks and payment form updates
- Add timezone fields migration and various UI improvements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-03 01:35:59 -05:00
parent ef58e9fc94
commit 5cef01ad0d
25 changed files with 2220 additions and 330 deletions

View File

@@ -35,6 +35,8 @@ export const useCurrentBusiness = () => {
logoUrl: data.logo_url,
emailLogoUrl: data.email_logo_url,
logoDisplayMode: data.logo_display_mode || 'text-only',
timezone: data.timezone || 'America/New_York',
timezoneDisplayMode: data.timezone_display_mode || 'business',
whitelabelEnabled: data.whitelabel_enabled,
plan: data.tier, // Map tier to plan
status: data.status,
@@ -82,11 +84,13 @@ export const useUpdateBusiness = () => {
// Map frontend fields to backend fields
if (updates.name) backendData.name = updates.name;
if (updates.primaryColor) backendData.primary_color = updates.primaryColor;
if (updates.secondaryColor) backendData.secondary_color = updates.secondaryColor;
if (updates.primaryColor !== undefined) backendData.primary_color = updates.primaryColor;
if (updates.secondaryColor !== undefined) backendData.secondary_color = updates.secondaryColor;
if (updates.logoUrl !== undefined) backendData.logo_url = updates.logoUrl;
if (updates.emailLogoUrl !== undefined) backendData.email_logo_url = updates.emailLogoUrl;
if (updates.logoDisplayMode !== undefined) backendData.logo_display_mode = updates.logoDisplayMode;
if (updates.timezone !== undefined) backendData.timezone = updates.timezone;
if (updates.timezoneDisplayMode !== undefined) backendData.timezone_display_mode = updates.timezoneDisplayMode;
if (updates.whitelabelEnabled !== undefined) {
backendData.whitelabel_enabled = updates.whitelabelEnabled;
}

View File

@@ -193,3 +193,134 @@ export const useCommunicationUsageStats = () => {
staleTime: 60 * 1000, // 1 minute
});
};
// =============================================================================
// Phone Number Management Hooks
// =============================================================================
export interface ProxyPhoneNumber {
id: number;
phone_number: string;
friendly_name: string;
status: 'available' | 'assigned' | 'reserved' | 'inactive';
monthly_fee_cents: number;
capabilities: {
voice: boolean;
sms: boolean;
mms: boolean;
};
assigned_at: string | null;
last_billed_at: string | null;
}
export interface AvailablePhoneNumber {
phone_number: string;
friendly_name: string;
locality: string;
region: string;
postal_code: string;
capabilities: {
voice: boolean;
sms: boolean;
mms: boolean;
};
monthly_cost_cents: number;
}
/**
* Hook to list phone numbers assigned to the tenant
*/
export const usePhoneNumbers = () => {
return useQuery<{ numbers: ProxyPhoneNumber[]; count: number }>({
queryKey: ['phoneNumbers'],
queryFn: async () => {
const { data } = await apiClient.get('/communication-credits/phone-numbers/');
return data;
},
staleTime: 30 * 1000,
});
};
/**
* Hook to search for available phone numbers from Twilio
*/
export const useSearchPhoneNumbers = () => {
return useMutation({
mutationFn: async (params: {
area_code?: string;
contains?: string;
country?: string;
limit?: number;
}) => {
const { data } = await apiClient.get('/communication-credits/phone-numbers/search/', {
params,
});
return data as { numbers: AvailablePhoneNumber[]; count: number };
},
});
};
/**
* Hook to purchase a phone number
*/
export const usePurchasePhoneNumber = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (params: { phone_number: string; friendly_name?: string }) => {
const { data } = await apiClient.post('/communication-credits/phone-numbers/purchase/', params);
return data as {
success: boolean;
phone_number: ProxyPhoneNumber;
balance_cents: number;
};
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['phoneNumbers'] });
queryClient.invalidateQueries({ queryKey: ['communicationCredits'] });
queryClient.invalidateQueries({ queryKey: ['creditTransactions'] });
},
});
};
/**
* Hook to release (delete) a phone number
*/
export const useReleasePhoneNumber = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (numberId: number) => {
const { data } = await apiClient.delete(`/communication-credits/phone-numbers/${numberId}/`);
return data as { success: boolean; message: string };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['phoneNumbers'] });
queryClient.invalidateQueries({ queryKey: ['communicationUsageStats'] });
},
});
};
/**
* Hook to change a phone number to a different one
*/
export const useChangePhoneNumber = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (params: { numberId: number; new_phone_number: string; friendly_name?: string }) => {
const { numberId, ...body } = params;
const { data } = await apiClient.post(`/communication-credits/phone-numbers/${numberId}/change/`, body);
return data as {
success: boolean;
phone_number: ProxyPhoneNumber;
balance_cents: number;
};
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['phoneNumbers'] });
queryClient.invalidateQueries({ queryKey: ['communicationCredits'] });
queryClient.invalidateQueries({ queryKey: ['creditTransactions'] });
},
});
};