)}
- {/* Assignee & Status (only visible for existing non-PLATFORM tickets) */}
- {ticket && ticketType !== 'PLATFORM' && (
+ {/* External Email Info - Show for platform tickets from external senders */}
+ {ticket && ticketType === 'PLATFORM' && isPlatformStaff && ticket.externalEmail && (
+
+
+
+ {t('tickets.externalSender', 'External Sender')}:
+ {ticket.externalName ? `${ticket.externalName} <${ticket.externalEmail}>` : ticket.externalEmail}
+
+
+ )}
+
+ {/* Assignee & Status - Show for existing tickets (non-PLATFORM OR platform admins viewing PLATFORM) */}
+ {ticket && (ticketType !== 'PLATFORM' || isPlatformAdmin) && (
)}
- {ticket && ticketType !== 'PLATFORM' && ( // Show update button for existing non-PLATFORM tickets
+ {ticket && (ticketType !== 'PLATFORM' || isPlatformAdmin) && ( // Show update button for existing tickets (non-PLATFORM OR platform admins)
- {/* Internal Note Form - Only show for non-PLATFORM tickets */}
- {ticketType !== 'PLATFORM' && (
+ {/* Internal Note Form - Show for non-PLATFORM tickets OR platform staff viewing PLATFORM tickets */}
+ {(ticketType !== 'PLATFORM' || isPlatformStaff) && (
{/* Tab Content */}
+ {activeTab === 'general' &&
}
{activeTab === 'stripe' &&
}
{activeTab === 'tiers' &&
}
{activeTab === 'oauth' &&
}
@@ -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 (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
Failed to load email settings
+
+
+ );
+ }
+
+ return (
+
+ {/* Email Processing Status */}
+
+
+
+ {t('platform.settings.emailProcessing', 'Support Email Processing')}
+
+
+
+
+ {emailSettings?.is_enabled ? (
+
+ ) : (
+
+ )}
+
+
Status
+
+ {emailSettings?.is_enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+ {emailSettings?.is_imap_configured ? (
+
+ ) : (
+
+ )}
+
+
IMAP (Inbound)
+
+ {emailSettings?.is_imap_configured ? 'Configured' : 'Not configured'}
+
+
+
+
+
+ {emailSettings?.is_smtp_configured ? (
+
+ ) : (
+
+ )}
+
+
SMTP (Outbound)
+
+ {emailSettings?.is_smtp_configured ? 'Configured' : 'Not configured'}
+
+
+
+
+
+
+
+
Last Check
+
+ {emailSettings?.last_check_at
+ ? new Date(emailSettings.last_check_at).toLocaleString()
+ : 'Never'}
+
+
+
+
+
+ {emailSettings?.last_error && (
+
+
+ Last Error: {emailSettings.last_error}
+
+
+ )}
+
+
+ Emails processed: {emailSettings?.emails_processed_count || 0}
+ Check interval: {emailSettings?.check_interval_seconds || 60}s
+
+
+
+ {/* IMAP Configuration */}
+
+
+
+ {isImapExpanded && (
+
+ {/* Enable/Disable Toggle */}
+
+
+
Enable Email Processing
+
+ Automatically fetch and process incoming support emails
+
+
+
+
+
+ {/* Server Settings */}
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
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"
+ />
+
+ Domain for reply-to addresses (e.g., support+ticket-123@domain)
+
+
+
+
+ {/* Test IMAP Button */}
+
+
+
+
+ {testImapMutation.isSuccess && (
+
+
+ {testImapMutation.data?.success ? (
+
+ ) : (
+
+ )}
+ {testImapMutation.data?.message}
+
+
+ )}
+
+ )}
+
+
+ {/* SMTP Configuration */}
+
+
+
+ {isSmtpExpanded && (
+
+ {/* Server Settings */}
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+ {/* Test SMTP Button */}
+
+
+
+
+ {testSmtpMutation.isSuccess && (
+
+
+ {testSmtpMutation.data?.success ? (
+
+ ) : (
+
+ )}
+ {testSmtpMutation.data?.message}
+
+
+ )}
+
+ )}
+
+
+ {/* Processing Settings */}
+
+
+
+ {t('platform.settings.processingSettings', 'Processing Settings')}
+
+
+
+
Email Fetching
+
+
+
+
+
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"
+ />
+
+ How often to check for new emails (10-3600 seconds)
+
+
+
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ {/* Status Messages */}
+ {updateMutation.isSuccess && (
+
+
+
+ Settings saved successfully
+
+
+ )}
+
+ {updateMutation.isError && (
+
+
+ Failed to save settings. Please try again.
+
+
+ )}
+
+ {fetchNowMutation.isSuccess && (
+
+
+
+ {fetchNowMutation.data?.message}
+
+
+ )}
+
+
+
+ );
+};
+
const StripeSettingsTab: React.FC = () => {
const { data: settings, isLoading, error } = usePlatformSettings();
const updateKeysMutation = useUpdateStripeKeys();
diff --git a/frontend/src/pages/platform/components/TenantInviteModal.tsx b/frontend/src/pages/platform/components/TenantInviteModal.tsx
index 7cf3c3c..0955b22 100644
--- a/frontend/src/pages/platform/components/TenantInviteModal.tsx
+++ b/frontend/src/pages/platform/components/TenantInviteModal.tsx
@@ -121,6 +121,16 @@ const TenantInviteModal: React.FC
= ({ isOpen, onClose }
data.permissions = inviteForm.permissions;
}
+ // Only include limits if at least one is enabled (boolean true or numeric value set)
+ const hasLimits = Object.entries(inviteForm.limits).some(([key, value]) => {
+ if (typeof value === 'boolean') return value === true;
+ if (typeof value === 'number') return true; // numeric limits are meaningful even if 0
+ return false;
+ });
+ if (hasLimits) {
+ data.limits = inviteForm.limits;
+ }
+
if (inviteForm.personal_message.trim()) {
data.personal_message = inviteForm.personal_message.trim();
}
@@ -320,24 +330,21 @@ const TenantInviteModal: React.FC = ({ isOpen, onClose }
- {/* Feature Limits (Not Yet Implemented) */}
+ {/* Feature Limits & Capabilities */}