- 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>
280 lines
10 KiB
TypeScript
280 lines
10 KiB
TypeScript
/**
|
|
* Ticket Email Address Manager Component
|
|
* Allows businesses to manage their ticket email addresses
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Mail,
|
|
Plus,
|
|
Trash2,
|
|
Edit,
|
|
CheckCircle,
|
|
XCircle,
|
|
Loader2,
|
|
Star,
|
|
TestTube,
|
|
RefreshCw,
|
|
Eye,
|
|
EyeOff,
|
|
} from 'lucide-react';
|
|
import {
|
|
useTicketEmailAddresses,
|
|
useDeleteTicketEmailAddress,
|
|
useTestImapConnection,
|
|
useTestSmtpConnection,
|
|
useFetchEmailsNow,
|
|
useSetAsDefault,
|
|
TicketEmailAddressListItem,
|
|
} from '../hooks/useTicketEmailAddresses';
|
|
import toast from 'react-hot-toast';
|
|
import TicketEmailAddressModal from './TicketEmailAddressModal';
|
|
|
|
const TicketEmailAddressManager: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingAddress, setEditingAddress] = useState<TicketEmailAddressListItem | null>(null);
|
|
const [showPasswords, setShowPasswords] = useState<Record<number, boolean>>({});
|
|
|
|
const { data: emailAddresses = [], isLoading } = useTicketEmailAddresses();
|
|
const deleteAddress = useDeleteTicketEmailAddress();
|
|
const testImap = useTestImapConnection();
|
|
const testSmtp = useTestSmtpConnection();
|
|
const fetchEmails = useFetchEmailsNow();
|
|
const setDefault = useSetAsDefault();
|
|
|
|
const handleAdd = () => {
|
|
setEditingAddress(null);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleEdit = (address: TicketEmailAddressListItem) => {
|
|
setEditingAddress(address);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (id: number, displayName: string) => {
|
|
if (confirm(`Are you sure you want to delete ${displayName}?`)) {
|
|
try {
|
|
await deleteAddress.mutateAsync(id);
|
|
toast.success(`${displayName} deleted successfully`);
|
|
} catch (error) {
|
|
toast.error('Failed to delete email address');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleTestImap = async (id: number, displayName: string) => {
|
|
toast.loading(`Testing IMAP connection for ${displayName}...`, { id: `imap-${id}` });
|
|
try {
|
|
const result = await testImap.mutateAsync(id);
|
|
if (result.success) {
|
|
toast.success(result.message, { id: `imap-${id}` });
|
|
} else {
|
|
toast.error(result.message, { id: `imap-${id}` });
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'IMAP test failed', { id: `imap-${id}` });
|
|
}
|
|
};
|
|
|
|
const handleTestSmtp = async (id: number, displayName: string) => {
|
|
toast.loading(`Testing SMTP connection for ${displayName}...`, { id: `smtp-${id}` });
|
|
try {
|
|
const result = await testSmtp.mutateAsync(id);
|
|
if (result.success) {
|
|
toast.success(result.message, { id: `smtp-${id}` });
|
|
} else {
|
|
toast.error(result.message, { id: `smtp-${id}` });
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'SMTP test failed', { id: `smtp-${id}` });
|
|
}
|
|
};
|
|
|
|
const handleFetchEmails = async (id: number, displayName: string) => {
|
|
toast.loading(`Fetching emails for ${displayName}...`, { id: `fetch-${id}` });
|
|
try {
|
|
const result = await fetchEmails.mutateAsync(id);
|
|
if (result.success) {
|
|
toast.success(
|
|
`${result.message}. Processed: ${result.processed || 0}, Errors: ${result.errors || 0}`,
|
|
{ id: `fetch-${id}`, duration: 5000 }
|
|
);
|
|
} else {
|
|
toast.error(result.message, { id: `fetch-${id}` });
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Failed to fetch emails', { id: `fetch-${id}` });
|
|
}
|
|
};
|
|
|
|
const handleSetDefault = async (id: number, displayName: string) => {
|
|
try {
|
|
const result = await setDefault.mutateAsync(id);
|
|
if (result.success) {
|
|
toast.success(result.message);
|
|
}
|
|
} catch (error) {
|
|
toast.error('Failed to set as default');
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<Mail className="w-6 h-6" />
|
|
Email Addresses
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
Manage email addresses for receiving and sending support tickets
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleAdd}
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Email Address
|
|
</button>
|
|
</div>
|
|
|
|
{/* Email Addresses List */}
|
|
{emailAddresses.length === 0 ? (
|
|
<div className="text-center py-12 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<Mail className="w-16 h-16 mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
No email addresses configured
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
Add your first email address to start receiving tickets via email
|
|
</p>
|
|
<button
|
|
onClick={handleAdd}
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Email Address
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{emailAddresses.map((address) => (
|
|
<div
|
|
key={address.id}
|
|
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6"
|
|
style={{ borderLeft: `4px solid ${address.color}` }}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{address.display_name}
|
|
</h3>
|
|
{address.is_default && (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300">
|
|
<Star className="w-3 h-3" />
|
|
Default
|
|
</span>
|
|
)}
|
|
{address.is_active ? (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
|
<CheckCircle className="w-3 h-3" />
|
|
Active
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
|
<XCircle className="w-3 h-3" />
|
|
Inactive
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-gray-600 dark:text-gray-400 mb-3">{address.email_address}</p>
|
|
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
|
|
<span>
|
|
Processed: <strong>{address.emails_processed_count}</strong> emails
|
|
</span>
|
|
{address.last_check_at && (
|
|
<span>
|
|
Last checked: {new Date(address.last_check_at).toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{!address.is_default && (
|
|
<button
|
|
onClick={() => handleSetDefault(address.id, address.display_name)}
|
|
disabled={setDefault.isPending}
|
|
className="p-2 text-gray-600 hover:text-yellow-600 dark:text-gray-400 dark:hover:text-yellow-400 transition-colors"
|
|
title="Set as default"
|
|
>
|
|
<Star className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => handleTestImap(address.id, address.display_name)}
|
|
disabled={testImap.isPending}
|
|
className="p-2 text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
|
|
title="Test IMAP"
|
|
>
|
|
<TestTube className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleFetchEmails(address.id, address.display_name)}
|
|
disabled={fetchEmails.isPending}
|
|
className="p-2 text-gray-600 hover:text-green-600 dark:text-gray-400 dark:hover:text-green-400 transition-colors"
|
|
title="Fetch emails now"
|
|
>
|
|
<RefreshCw className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleEdit(address)}
|
|
className="p-2 text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
|
|
title="Edit"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(address.id, address.display_name)}
|
|
disabled={deleteAddress.isPending}
|
|
className="p-2 text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 transition-colors"
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal */}
|
|
{isModalOpen && (
|
|
<TicketEmailAddressModal
|
|
address={editingAddress}
|
|
onClose={() => {
|
|
setIsModalOpen(false);
|
|
setEditingAddress(null);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TicketEmailAddressManager;
|