feat: Multi-email ticketing system with platform email addresses
- Add PlatformEmailAddress model for managing platform-level email addresses - Add TicketEmailAddress model for tenant-level email addresses - Create MailServerService for IMAP integration with mail.talova.net - Implement PlatformEmailReceiver for processing incoming platform emails - Add email autoconfiguration for Mozilla, Microsoft, and Apple clients - Add configurable email polling interval in platform settings - Add "Check Emails" button on support page for manual refresh - Add ticket counts to status tabs on support page - Add platform email addresses management page - Add Privacy Policy and Terms of Service pages - Add robots.txt for SEO - Restrict email addresses to smoothschedule.com domain only 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
207
frontend/src/hooks/usePlatformEmailAddresses.ts
Normal file
207
frontend/src/hooks/usePlatformEmailAddresses.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* React Query hooks for Platform Email Addresses
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getPlatformEmailAddresses,
|
||||
getPlatformEmailAddress,
|
||||
createPlatformEmailAddress,
|
||||
updatePlatformEmailAddress,
|
||||
deletePlatformEmailAddress,
|
||||
removeLocalPlatformEmailAddress,
|
||||
syncPlatformEmailAddress,
|
||||
testImapConnection,
|
||||
testSmtpConnection,
|
||||
setAsDefault,
|
||||
testMailServerConnection,
|
||||
getMailServerAccounts,
|
||||
getAvailableDomains,
|
||||
getAssignableUsers,
|
||||
importFromMailServer,
|
||||
PlatformEmailAddressListItem,
|
||||
PlatformEmailAddress,
|
||||
PlatformEmailAddressCreate,
|
||||
PlatformEmailAddressUpdate,
|
||||
} from '../api/platformEmailAddresses';
|
||||
|
||||
export type { PlatformEmailAddressListItem, PlatformEmailAddress };
|
||||
|
||||
const QUERY_KEY = 'platformEmailAddresses';
|
||||
|
||||
/**
|
||||
* Hook to fetch all platform email addresses
|
||||
*/
|
||||
export const usePlatformEmailAddresses = () => {
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: getPlatformEmailAddresses,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch a single platform email address
|
||||
*/
|
||||
export const usePlatformEmailAddress = (id: number) => {
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, id],
|
||||
queryFn: () => getPlatformEmailAddress(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to create a new platform email address
|
||||
*/
|
||||
export const useCreatePlatformEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: PlatformEmailAddressCreate) => createPlatformEmailAddress(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update a platform email address
|
||||
*/
|
||||
export const useUpdatePlatformEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: PlatformEmailAddressUpdate }) =>
|
||||
updatePlatformEmailAddress(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete a platform email address (also removes from mail server)
|
||||
*/
|
||||
export const useDeletePlatformEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deletePlatformEmailAddress(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to remove email address from database only (keeps mail server account)
|
||||
*/
|
||||
export const useRemoveLocalPlatformEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => removeLocalPlatformEmailAddress(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to sync a platform email address to the mail server
|
||||
*/
|
||||
export const useSyncPlatformEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => syncPlatformEmailAddress(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to test IMAP connection
|
||||
*/
|
||||
export const useTestImapConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => testImapConnection(id),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to test SMTP connection
|
||||
*/
|
||||
export const useTestSmtpConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => testSmtpConnection(id),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to set email address as default
|
||||
*/
|
||||
export const useSetAsDefault = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => setAsDefault(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to test mail server SSH connection
|
||||
*/
|
||||
export const useTestMailServerConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: testMailServerConnection,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get mail server accounts
|
||||
*/
|
||||
export const useMailServerAccounts = () => {
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, 'mailServerAccounts'],
|
||||
queryFn: getMailServerAccounts,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get available email domains
|
||||
*/
|
||||
export const useAvailableDomains = () => {
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, 'availableDomains'],
|
||||
queryFn: getAvailableDomains,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get assignable users
|
||||
*/
|
||||
export const useAssignableUsers = () => {
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, 'assignableUsers'],
|
||||
queryFn: getAssignableUsers,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to import email addresses from the mail server
|
||||
*/
|
||||
export const useImportFromMailServer = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: importFromMailServer,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -15,9 +15,14 @@ export interface PlatformSettings {
|
||||
stripe_validation_error: string;
|
||||
has_stripe_keys: boolean;
|
||||
stripe_keys_from_env: boolean;
|
||||
email_check_interval_minutes: number;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface GeneralSettingsUpdate {
|
||||
email_check_interval_minutes?: number;
|
||||
}
|
||||
|
||||
export interface StripeKeysUpdate {
|
||||
stripe_secret_key?: string;
|
||||
stripe_publishable_key?: string;
|
||||
@@ -35,10 +40,14 @@ export interface SubscriptionPlan {
|
||||
price_yearly: string | null;
|
||||
business_tier: string;
|
||||
features: string[];
|
||||
limits: Record<string, any>;
|
||||
permissions: Record<string, boolean>;
|
||||
transaction_fee_percent: string;
|
||||
transaction_fee_fixed: string;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
is_most_popular: boolean;
|
||||
show_price: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -51,10 +60,14 @@ export interface SubscriptionPlanCreate {
|
||||
price_yearly?: number | null;
|
||||
business_tier?: string;
|
||||
features?: string[];
|
||||
limits?: Record<string, any>;
|
||||
permissions?: Record<string, boolean>;
|
||||
transaction_fee_percent?: number;
|
||||
transaction_fee_fixed?: number;
|
||||
is_active?: boolean;
|
||||
is_public?: boolean;
|
||||
is_most_popular?: boolean;
|
||||
show_price?: boolean;
|
||||
create_stripe_product?: boolean;
|
||||
stripe_product_id?: string;
|
||||
stripe_price_id?: string;
|
||||
@@ -74,6 +87,23 @@ export const usePlatformSettings = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update general platform settings
|
||||
*/
|
||||
export const useUpdateGeneralSettings = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: GeneralSettingsUpdate) => {
|
||||
const { data } = await apiClient.post('/platform/settings/general/', settings);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(['platformSettings'], data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update platform Stripe keys
|
||||
*/
|
||||
@@ -148,7 +178,7 @@ export const useUpdateSubscriptionPlan = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...updates }: Partial<SubscriptionPlan> & { id: number }) => {
|
||||
mutationFn: async ({ id, ...updates }: Partial<SubscriptionPlanCreate> & { id: number }) => {
|
||||
const { data } = await apiClient.patch(`/platform/subscription-plans/${id}/`, updates);
|
||||
return data;
|
||||
},
|
||||
|
||||
141
frontend/src/hooks/useTicketEmailAddresses.ts
Normal file
141
frontend/src/hooks/useTicketEmailAddresses.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* React Query hooks for ticket email addresses
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
getTicketEmailAddresses,
|
||||
getTicketEmailAddress,
|
||||
createTicketEmailAddress,
|
||||
updateTicketEmailAddress,
|
||||
deleteTicketEmailAddress,
|
||||
testImapConnection,
|
||||
testSmtpConnection,
|
||||
fetchEmailsNow,
|
||||
setAsDefault,
|
||||
TicketEmailAddress,
|
||||
TicketEmailAddressListItem,
|
||||
TicketEmailAddressCreate,
|
||||
} from '../api/ticketEmailAddresses';
|
||||
|
||||
const QUERY_KEY = 'ticketEmailAddresses';
|
||||
|
||||
/**
|
||||
* Hook to fetch all ticket email addresses
|
||||
*/
|
||||
export const useTicketEmailAddresses = () => {
|
||||
return useQuery<TicketEmailAddressListItem[]>({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: getTicketEmailAddresses,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch a specific ticket email address
|
||||
*/
|
||||
export const useTicketEmailAddress = (id: number) => {
|
||||
return useQuery<TicketEmailAddress>({
|
||||
queryKey: [QUERY_KEY, id],
|
||||
queryFn: () => getTicketEmailAddress(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to create a new ticket email address
|
||||
*/
|
||||
export const useCreateTicketEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: TicketEmailAddressCreate) => createTicketEmailAddress(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update an existing ticket email address
|
||||
*/
|
||||
export const useUpdateTicketEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<TicketEmailAddressCreate> }) =>
|
||||
updateTicketEmailAddress(id, data),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, variables.id] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete a ticket email address
|
||||
*/
|
||||
export const useDeleteTicketEmailAddress = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTicketEmailAddress(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to test IMAP connection
|
||||
*/
|
||||
export const useTestImapConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => testImapConnection(id),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to test SMTP connection
|
||||
*/
|
||||
export const useTestSmtpConnection = () => {
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => testSmtpConnection(id),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to manually fetch emails
|
||||
*/
|
||||
export const useFetchEmailsNow = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => fetchEmailsNow(id),
|
||||
onSuccess: () => {
|
||||
// Refresh the email addresses list to update the last_check_at timestamp
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
// Also invalidate tickets query to show any new tickets
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to set an email address as default
|
||||
*/
|
||||
export const useSetAsDefault = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => setAsDefault(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type {
|
||||
TicketEmailAddress,
|
||||
TicketEmailAddressListItem,
|
||||
TicketEmailAddressCreate,
|
||||
};
|
||||
@@ -274,3 +274,19 @@ export const useCannedResponses = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to manually refresh/check for new ticket emails
|
||||
*/
|
||||
export const useRefreshTicketEmails = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ticketsApi.refreshTicketEmails,
|
||||
onSuccess: (data) => {
|
||||
// Refresh tickets list if any emails were processed
|
||||
if (data.processed > 0) {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user