feat: Email templates, bulk delete, communication credits, plan features
- Add email template presets for Browse Templates tab (12 templates) - Add bulk selection and deletion for My Templates tab - Add communication credits system with Twilio integration - Add payment views for credit purchases and auto-reload - Add SMS reminder and masked calling plan permissions - Fix appointment status mapping (frontend/backend mismatch) - Clear masquerade stack on login/logout for session hygiene - Update platform settings with credit configuration - Add new migrations for Twilio and Stripe payment fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,9 @@ export const useLogin = () => {
|
||||
setCookie('access_token', data.access, 7);
|
||||
setCookie('refresh_token', data.refresh, 7);
|
||||
|
||||
// Clear any existing masquerade stack - this is a fresh login
|
||||
localStorage.removeItem('masquerade_stack');
|
||||
|
||||
// Set user in cache
|
||||
queryClient.setQueryData(['currentUser'], data.user);
|
||||
},
|
||||
@@ -91,6 +94,9 @@ export const useLogout = () => {
|
||||
deleteCookie('access_token');
|
||||
deleteCookie('refresh_token');
|
||||
|
||||
// Clear masquerade stack
|
||||
localStorage.removeItem('masquerade_stack');
|
||||
|
||||
// Clear user cache
|
||||
queryClient.removeQueries({ queryKey: ['currentUser'] });
|
||||
queryClient.clear();
|
||||
|
||||
195
frontend/src/hooks/useCommunicationCredits.ts
Normal file
195
frontend/src/hooks/useCommunicationCredits.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Communication Credits Hooks
|
||||
* For managing business SMS/calling credits and auto-reload settings
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import apiClient from '../api/client';
|
||||
|
||||
export interface CommunicationCredits {
|
||||
id: number;
|
||||
balance_cents: number;
|
||||
auto_reload_enabled: boolean;
|
||||
auto_reload_threshold_cents: number;
|
||||
auto_reload_amount_cents: number;
|
||||
low_balance_warning_cents: number;
|
||||
low_balance_warning_sent: boolean;
|
||||
stripe_payment_method_id: string;
|
||||
last_twilio_sync_at: string | null;
|
||||
total_loaded_cents: number;
|
||||
total_spent_cents: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreditTransaction {
|
||||
id: number;
|
||||
amount_cents: number;
|
||||
balance_after_cents: number;
|
||||
transaction_type: 'manual' | 'auto_reload' | 'usage' | 'refund' | 'adjustment' | 'promo';
|
||||
description: string;
|
||||
reference_type: string;
|
||||
reference_id: string;
|
||||
stripe_charge_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface UpdateCreditsSettings {
|
||||
auto_reload_enabled?: boolean;
|
||||
auto_reload_threshold_cents?: number;
|
||||
auto_reload_amount_cents?: number;
|
||||
low_balance_warning_cents?: number;
|
||||
stripe_payment_method_id?: string;
|
||||
}
|
||||
|
||||
export interface AddCreditsRequest {
|
||||
amount_cents: number;
|
||||
payment_method_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get communication credits for current business
|
||||
*/
|
||||
export const useCommunicationCredits = () => {
|
||||
return useQuery<CommunicationCredits>({
|
||||
queryKey: ['communicationCredits'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/communication-credits/');
|
||||
return data;
|
||||
},
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get credit transaction history
|
||||
*/
|
||||
export const useCreditTransactions = (page = 1, limit = 20) => {
|
||||
return useQuery<{ results: CreditTransaction[]; count: number; next: string | null; previous: string | null }>({
|
||||
queryKey: ['creditTransactions', page, limit],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/communication-credits/transactions/', {
|
||||
params: { page, limit },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update credit settings (auto-reload, thresholds, etc.)
|
||||
*/
|
||||
export const useUpdateCreditsSettings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: UpdateCreditsSettings) => {
|
||||
const { data } = await apiClient.patch('/communication-credits/settings/', settings);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(['communicationCredits'], data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to add credits (manual top-up)
|
||||
*/
|
||||
export const useAddCredits = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (request: AddCreditsRequest) => {
|
||||
const { data } = await apiClient.post('/communication-credits/add/', request);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['communicationCredits'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['creditTransactions'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to create a payment intent for credit purchase
|
||||
*/
|
||||
export const useCreatePaymentIntent = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (amount_cents: number) => {
|
||||
const { data } = await apiClient.post('/communication-credits/create-payment-intent/', {
|
||||
amount_cents,
|
||||
});
|
||||
return data as { client_secret: string; payment_intent_id: string };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to confirm a payment after client-side processing
|
||||
*/
|
||||
export const useConfirmPayment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (params: { payment_intent_id: string; save_payment_method?: boolean }) => {
|
||||
const { data } = await apiClient.post('/communication-credits/confirm-payment/', params);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['communicationCredits'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['creditTransactions'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to set up Stripe payment method for auto-reload
|
||||
*/
|
||||
export const useSetupPaymentMethod = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiClient.post('/communication-credits/setup-payment-method/');
|
||||
return data as { client_secret: string };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to save a payment method after setup
|
||||
*/
|
||||
export const useSavePaymentMethod = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payment_method_id: string) => {
|
||||
const { data } = await apiClient.post('/communication-credits/save-payment-method/', {
|
||||
payment_method_id,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['communicationCredits'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get communication usage stats
|
||||
*/
|
||||
export const useCommunicationUsageStats = () => {
|
||||
return useQuery<{
|
||||
sms_sent_this_month: number;
|
||||
voice_minutes_this_month: number;
|
||||
proxy_numbers_active: number;
|
||||
estimated_cost_cents: number;
|
||||
}>({
|
||||
queryKey: ['communicationUsageStats'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/communication-credits/usage-stats/');
|
||||
return data;
|
||||
},
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
});
|
||||
};
|
||||
@@ -44,6 +44,17 @@ export interface SubscriptionPlan {
|
||||
permissions: Record<string, boolean>;
|
||||
transaction_fee_percent: string;
|
||||
transaction_fee_fixed: string;
|
||||
// Communication pricing
|
||||
sms_enabled: boolean;
|
||||
sms_price_per_message_cents: number;
|
||||
masked_calling_enabled: boolean;
|
||||
masked_calling_price_per_minute_cents: number;
|
||||
proxy_number_enabled: boolean;
|
||||
proxy_number_monthly_fee_cents: number;
|
||||
// Default credit settings
|
||||
default_auto_reload_enabled: boolean;
|
||||
default_auto_reload_threshold_cents: number;
|
||||
default_auto_reload_amount_cents: number;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
is_most_popular: boolean;
|
||||
@@ -64,6 +75,17 @@ export interface SubscriptionPlanCreate {
|
||||
permissions?: Record<string, boolean>;
|
||||
transaction_fee_percent?: number;
|
||||
transaction_fee_fixed?: number;
|
||||
// Communication pricing
|
||||
sms_enabled?: boolean;
|
||||
sms_price_per_message_cents?: number;
|
||||
masked_calling_enabled?: boolean;
|
||||
masked_calling_price_per_minute_cents?: number;
|
||||
proxy_number_enabled?: boolean;
|
||||
proxy_number_monthly_fee_cents?: number;
|
||||
// Default credit settings
|
||||
default_auto_reload_enabled?: boolean;
|
||||
default_auto_reload_threshold_cents?: number;
|
||||
default_auto_reload_amount_cents?: number;
|
||||
is_active?: boolean;
|
||||
is_public?: boolean;
|
||||
is_most_popular?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user