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:
poduck
2025-11-29 18:28:29 -05:00
parent 0c7d76e264
commit cfc1b36ada
94 changed files with 13419 additions and 1121 deletions

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Mail, ExternalLink } from 'lucide-react';
import api from '../api/client';
import { EmailTemplate } from '../types';
interface EmailTemplateSelectorProps {
value: string | number | undefined;
onChange: (templateId: string | number | undefined) => void;
category?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
className?: string;
}
const EmailTemplateSelector: React.FC<EmailTemplateSelectorProps> = ({
value,
onChange,
category,
placeholder,
required = false,
disabled = false,
className = '',
}) => {
const { t } = useTranslation();
// Fetch email templates
const { data: templates = [], isLoading } = useQuery<EmailTemplate[]>({
queryKey: ['email-templates-list', category],
queryFn: async () => {
const params = new URLSearchParams();
if (category) params.append('category', category);
const { data } = await api.get(`/api/email-templates/?${params.toString()}`);
return data.map((t: any) => ({
id: String(t.id),
name: t.name,
description: t.description,
category: t.category,
scope: t.scope,
updatedAt: t.updated_at,
}));
},
});
const selectedTemplate = templates.find(t => String(t.id) === String(value));
return (
<div className={`space-y-2 ${className}`}>
<div className="relative">
<select
value={value || ''}
onChange={(e) => onChange(e.target.value || undefined)}
disabled={disabled || isLoading}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500 disabled:opacity-50 disabled:cursor-not-allowed appearance-none"
>
<option value="">
{isLoading
? t('common.loading', 'Loading...')
: placeholder || t('emailTemplates.selectTemplate', 'Select a template...')}
</option>
{templates.map((template) => (
<option key={template.id} value={template.id}>
{template.name}
{template.category !== 'OTHER' && ` (${template.category})`}
</option>
))}
</select>
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 pointer-events-none" />
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{/* Selected template info */}
{selectedTemplate && (
<div className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-800 rounded text-sm">
<span className="text-gray-600 dark:text-gray-400 truncate">
{selectedTemplate.description || selectedTemplate.name}
</span>
<a
href={`#/email-templates`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-brand-600 dark:text-brand-400 hover:underline ml-2 flex-shrink-0"
>
<ExternalLink className="h-3 w-3" />
{t('common.edit', 'Edit')}
</a>
</div>
)}
{/* Empty state with link to create */}
{!isLoading && templates.length === 0 && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{t('emailTemplates.noTemplatesYet', 'No email templates yet.')}{' '}
<a
href="#/email-templates"
className="text-brand-600 dark:text-brand-400 hover:underline"
>
{t('emailTemplates.createFirst', 'Create your first template')}
</a>
</div>
)}
</div>
);
};
export default EmailTemplateSelector;