feat: Add SMTP settings and collapsible email configuration UI
- Add SMTP fields to TicketEmailSettings model (host, port, TLS/SSL, credentials, from email/name) - Update serializers with SMTP fields and is_smtp_configured flag - Add TicketEmailTestSmtpView for testing SMTP connections - Update frontend API types and hooks for SMTP settings - Add collapsible IMAP and SMTP configuration sections with "Configured" badges - Fix TypeScript errors in mockData.ts (missing required fields, type mismatches) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,12 @@ import {
|
||||
Lock,
|
||||
Users,
|
||||
ExternalLink,
|
||||
Mail,
|
||||
Clock,
|
||||
Server,
|
||||
Play,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
usePlatformSettings,
|
||||
@@ -42,14 +48,23 @@ import {
|
||||
usePlatformOAuthSettings,
|
||||
useUpdatePlatformOAuthSettings,
|
||||
} from '../../hooks/usePlatformOAuth';
|
||||
import {
|
||||
useTicketEmailSettings,
|
||||
useUpdateTicketEmailSettings,
|
||||
useTestImapConnection,
|
||||
useTestSmtpConnection,
|
||||
useFetchEmailsNow,
|
||||
} from '../../hooks/useTicketEmailSettings';
|
||||
import { Send } from 'lucide-react';
|
||||
|
||||
type TabType = 'stripe' | 'tiers' | 'oauth';
|
||||
type TabType = 'general' | 'stripe' | 'tiers' | 'oauth';
|
||||
|
||||
const PlatformSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('stripe');
|
||||
const [activeTab, setActiveTab] = useState<TabType>('general');
|
||||
|
||||
const tabs: { id: TabType; label: string; icon: React.ElementType }[] = [
|
||||
{ id: 'general', label: t('platform.settings.general', 'General'), icon: Settings },
|
||||
{ id: 'stripe', label: 'Stripe', icon: CreditCard },
|
||||
{ id: 'tiers', label: t('platform.settings.tiersPricing'), icon: Layers },
|
||||
{ id: 'oauth', label: t('platform.settings.oauthProviders'), icon: Users },
|
||||
@@ -94,6 +109,7 @@ const PlatformSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'general' && <GeneralSettingsTab />}
|
||||
{activeTab === 'stripe' && <StripeSettingsTab />}
|
||||
{activeTab === 'tiers' && <TiersSettingsTab />}
|
||||
{activeTab === 'oauth' && <OAuthSettingsTab />}
|
||||
@@ -101,6 +117,692 @@ const PlatformSettings: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralSettingsTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: emailSettings, isLoading, error } = useTicketEmailSettings();
|
||||
const updateMutation = useUpdateTicketEmailSettings();
|
||||
const testImapMutation = useTestImapConnection();
|
||||
const testSmtpMutation = useTestSmtpConnection();
|
||||
const fetchNowMutation = useFetchEmailsNow();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
// IMAP settings
|
||||
imap_host: '',
|
||||
imap_port: 993,
|
||||
imap_use_ssl: true,
|
||||
imap_username: '',
|
||||
imap_password: '',
|
||||
imap_folder: 'INBOX',
|
||||
// SMTP settings
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_use_tls: true,
|
||||
smtp_use_ssl: false,
|
||||
smtp_username: '',
|
||||
smtp_password: '',
|
||||
smtp_from_email: '',
|
||||
smtp_from_name: '',
|
||||
// General settings
|
||||
support_email_address: '',
|
||||
support_email_domain: '',
|
||||
is_enabled: false,
|
||||
delete_after_processing: true,
|
||||
check_interval_seconds: 60,
|
||||
});
|
||||
|
||||
const [showImapPassword, setShowImapPassword] = useState(false);
|
||||
const [showSmtpPassword, setShowSmtpPassword] = useState(false);
|
||||
const [isImapExpanded, setIsImapExpanded] = useState(false);
|
||||
const [isSmtpExpanded, setIsSmtpExpanded] = useState(false);
|
||||
|
||||
// Update form when settings load
|
||||
React.useEffect(() => {
|
||||
if (emailSettings) {
|
||||
setFormData({
|
||||
// IMAP settings
|
||||
imap_host: emailSettings.imap_host || '',
|
||||
imap_port: emailSettings.imap_port || 993,
|
||||
imap_use_ssl: emailSettings.imap_use_ssl ?? true,
|
||||
imap_username: emailSettings.imap_username || '',
|
||||
imap_password: '', // Don't prefill password
|
||||
imap_folder: emailSettings.imap_folder || 'INBOX',
|
||||
// SMTP settings
|
||||
smtp_host: emailSettings.smtp_host || '',
|
||||
smtp_port: emailSettings.smtp_port || 587,
|
||||
smtp_use_tls: emailSettings.smtp_use_tls ?? true,
|
||||
smtp_use_ssl: emailSettings.smtp_use_ssl ?? false,
|
||||
smtp_username: emailSettings.smtp_username || '',
|
||||
smtp_password: '', // Don't prefill password
|
||||
smtp_from_email: emailSettings.smtp_from_email || '',
|
||||
smtp_from_name: emailSettings.smtp_from_name || '',
|
||||
// General settings
|
||||
support_email_address: emailSettings.support_email_address || '',
|
||||
support_email_domain: emailSettings.support_email_domain || '',
|
||||
is_enabled: emailSettings.is_enabled ?? false,
|
||||
delete_after_processing: emailSettings.delete_after_processing ?? true,
|
||||
check_interval_seconds: emailSettings.check_interval_seconds || 60,
|
||||
});
|
||||
}
|
||||
}, [emailSettings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
// Only send passwords if they were changed
|
||||
const dataToSend = { ...formData };
|
||||
if (!dataToSend.imap_password) {
|
||||
delete (dataToSend as any).imap_password;
|
||||
}
|
||||
if (!dataToSend.smtp_password) {
|
||||
delete (dataToSend as any).smtp_password;
|
||||
}
|
||||
await updateMutation.mutateAsync(dataToSend);
|
||||
};
|
||||
|
||||
const handleTestImap = async () => {
|
||||
await testImapMutation.mutateAsync();
|
||||
};
|
||||
|
||||
const handleTestSmtp = async () => {
|
||||
await testSmtpMutation.mutateAsync();
|
||||
};
|
||||
|
||||
const handleFetchNow = async () => {
|
||||
await fetchNowMutation.mutateAsync();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-red-700 dark:text-red-400">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>Failed to load email settings</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Email Processing Status */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Mail className="w-5 h-5" />
|
||||
{t('platform.settings.emailProcessing', 'Support Email Processing')}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
{emailSettings?.is_enabled ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Status</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{emailSettings?.is_enabled ? 'Enabled' : 'Disabled'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
{emailSettings?.is_imap_configured ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">IMAP (Inbound)</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{emailSettings?.is_imap_configured ? 'Configured' : 'Not configured'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
{emailSettings?.is_smtp_configured ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">SMTP (Outbound)</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{emailSettings?.is_smtp_configured ? 'Configured' : 'Not configured'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Last Check</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{emailSettings?.last_check_at
|
||||
? new Date(emailSettings.last_check_at).toLocaleString()
|
||||
: 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{emailSettings?.last_error && (
|
||||
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">
|
||||
<span className="font-medium">Last Error:</span> {emailSettings.last_error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Emails processed: <strong>{emailSettings?.emails_processed_count || 0}</strong></span>
|
||||
<span>Check interval: <strong>{emailSettings?.check_interval_seconds || 60}s</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IMAP Configuration */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsImapExpanded(!isImapExpanded)}
|
||||
className="w-full p-6 flex items-center justify-between text-left"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Server className="w-5 h-5" />
|
||||
{t('platform.settings.imapConfig', 'IMAP Server Configuration (Inbound)')}
|
||||
{emailSettings?.is_imap_configured && (
|
||||
<span className="ml-2 px-2 py-0.5 text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded">
|
||||
Configured
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{isImapExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isImapExpanded && (
|
||||
<div className="px-6 pb-6 space-y-4">
|
||||
{/* Enable/Disable Toggle */}
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">Enable Email Processing</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Automatically fetch and process incoming support emails
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_enabled}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, is_enabled: e.target.checked }))}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Server Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
IMAP Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.imap_host}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, imap_host: e.target.value }))}
|
||||
placeholder="mail.talova.net"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.imap_port}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, imap_port: parseInt(e.target.value) || 993 }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end pb-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.imap_use_ssl}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, imap_use_ssl: e.target.checked }))}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Use SSL</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.imap_username}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, imap_username: e.target.value }))}
|
||||
placeholder="support@yourdomain.com"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showImapPassword ? 'text' : 'password'}
|
||||
value={formData.imap_password}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, imap_password: e.target.value }))}
|
||||
placeholder={emailSettings?.imap_password_masked || 'Enter password'}
|
||||
className="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowImapPassword(!showImapPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
{showImapPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Folder
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.imap_folder}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, imap_folder: e.target.value }))}
|
||||
placeholder="INBOX"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Support Email Domain
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.support_email_domain}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, support_email_domain: e.target.value }))}
|
||||
placeholder="mail.talova.net"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Domain for reply-to addresses (e.g., support+ticket-123@domain)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test IMAP Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleTestImap}
|
||||
disabled={testImapMutation.isPending || !formData.imap_host}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
|
||||
>
|
||||
{testImapMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Testing IMAP...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Test IMAP Connection
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{testImapMutation.isSuccess && (
|
||||
<div className={`p-3 rounded-lg ${
|
||||
testImapMutation.data?.success
|
||||
? 'bg-green-50 dark:bg-green-900/20'
|
||||
: 'bg-red-50 dark:bg-red-900/20'
|
||||
}`}>
|
||||
<p className={`text-sm flex items-center gap-2 ${
|
||||
testImapMutation.data?.success
|
||||
? 'text-green-700 dark:text-green-400'
|
||||
: 'text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{testImapMutation.data?.success ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
)}
|
||||
{testImapMutation.data?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SMTP Configuration */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSmtpExpanded(!isSmtpExpanded)}
|
||||
className="w-full p-6 flex items-center justify-between text-left"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Send className="w-5 h-5" />
|
||||
{t('platform.settings.smtpConfig', 'SMTP Server Configuration (Outbound)')}
|
||||
{emailSettings?.is_smtp_configured && (
|
||||
<span className="ml-2 px-2 py-0.5 text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded">
|
||||
Configured
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{isSmtpExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isSmtpExpanded && (
|
||||
<div className="px-6 pb-6 space-y-4">
|
||||
{/* Server Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
SMTP Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.smtp_host}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_host: e.target.value }))}
|
||||
placeholder="smtp.example.com"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.smtp_port}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_port: parseInt(e.target.value) || 587 }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end pb-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.smtp_use_tls}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_use_tls: e.target.checked, smtp_use_ssl: e.target.checked ? false : prev.smtp_use_ssl }))}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">TLS</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-end pb-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.smtp_use_ssl}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_use_ssl: e.target.checked, smtp_use_tls: e.target.checked ? false : prev.smtp_use_tls }))}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">SSL</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.smtp_username}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_username: e.target.value }))}
|
||||
placeholder="user@example.com"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showSmtpPassword ? 'text' : 'password'}
|
||||
value={formData.smtp_password}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_password: e.target.value }))}
|
||||
placeholder={emailSettings?.smtp_password_masked || 'Enter password'}
|
||||
className="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSmtpPassword(!showSmtpPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
{showSmtpPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
From Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.smtp_from_email}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_from_email: e.target.value }))}
|
||||
placeholder="support@yourdomain.com"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
From Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.smtp_from_name}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, smtp_from_name: e.target.value }))}
|
||||
placeholder="SmoothSchedule Support"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test SMTP Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleTestSmtp}
|
||||
disabled={testSmtpMutation.isPending || !formData.smtp_host}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
|
||||
>
|
||||
{testSmtpMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Testing SMTP...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Test SMTP Connection
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{testSmtpMutation.isSuccess && (
|
||||
<div className={`p-3 rounded-lg ${
|
||||
testSmtpMutation.data?.success
|
||||
? 'bg-green-50 dark:bg-green-900/20'
|
||||
: 'bg-red-50 dark:bg-red-900/20'
|
||||
}`}>
|
||||
<p className={`text-sm flex items-center gap-2 ${
|
||||
testSmtpMutation.data?.success
|
||||
? 'text-green-700 dark:text-green-400'
|
||||
: 'text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{testSmtpMutation.data?.success ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
)}
|
||||
{testSmtpMutation.data?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Processing Settings */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
{t('platform.settings.processingSettings', 'Processing Settings')}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium text-gray-900 dark:text-white mb-4">Email Fetching</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Check Interval (seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
max="3600"
|
||||
value={formData.check_interval_seconds}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, check_interval_seconds: parseInt(e.target.value) || 60 }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
How often to check for new emails (10-3600 seconds)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end pb-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.delete_after_processing}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, delete_after_processing: e.target.checked }))}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Delete emails after processing
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={updateMutation.isPending}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{updateMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Settings'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleFetchNow}
|
||||
disabled={fetchNowMutation.isPending || !emailSettings?.is_imap_configured}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
|
||||
>
|
||||
{fetchNowMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Fetching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4" />
|
||||
Fetch Now
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status Messages */}
|
||||
{updateMutation.isSuccess && (
|
||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<p className="text-sm text-green-700 dark:text-green-400 flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Settings saved successfully
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updateMutation.isError && (
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">
|
||||
Failed to save settings. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fetchNowMutation.isSuccess && (
|
||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<p className="text-sm text-green-700 dark:text-green-400 flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{fetchNowMutation.data?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StripeSettingsTab: React.FC = () => {
|
||||
const { data: settings, isLoading, error } = usePlatformSettings();
|
||||
const updateKeysMutation = useUpdateStripeKeys();
|
||||
|
||||
Reference in New Issue
Block a user