- 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>
776 lines
31 KiB
TypeScript
776 lines
31 KiB
TypeScript
/**
|
|
* Platform Email Address Manager Component
|
|
* Manages email addresses hosted on mail.talova.net via SSH
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Mail,
|
|
Plus,
|
|
Trash2,
|
|
Edit,
|
|
CheckCircle,
|
|
XCircle,
|
|
Loader2,
|
|
Star,
|
|
TestTube,
|
|
RefreshCw,
|
|
Server,
|
|
AlertTriangle,
|
|
X,
|
|
Download,
|
|
Unlink,
|
|
} from 'lucide-react';
|
|
import {
|
|
usePlatformEmailAddresses,
|
|
useDeletePlatformEmailAddress,
|
|
useRemoveLocalPlatformEmailAddress,
|
|
useTestImapConnection,
|
|
useTestSmtpConnection,
|
|
useSyncPlatformEmailAddress,
|
|
useSetAsDefault,
|
|
useTestMailServerConnection,
|
|
useCreatePlatformEmailAddress,
|
|
useUpdatePlatformEmailAddress,
|
|
useAssignableUsers,
|
|
useImportFromMailServer,
|
|
PlatformEmailAddressListItem,
|
|
} from '../hooks/usePlatformEmailAddresses';
|
|
import toast from 'react-hot-toast';
|
|
|
|
// Color options for email addresses
|
|
const COLOR_OPTIONS = [
|
|
'#3b82f6', // blue
|
|
'#10b981', // green
|
|
'#f59e0b', // amber
|
|
'#ef4444', // red
|
|
'#8b5cf6', // violet
|
|
'#ec4899', // pink
|
|
'#06b6d4', // cyan
|
|
'#f97316', // orange
|
|
];
|
|
|
|
interface EmailAddressFormData {
|
|
display_name: string;
|
|
sender_name: string;
|
|
assigned_user_id: number | null;
|
|
local_part: string;
|
|
domain: string;
|
|
color: string;
|
|
password: string;
|
|
is_active: boolean;
|
|
is_default: boolean;
|
|
}
|
|
|
|
interface ConfirmModalState {
|
|
isOpen: boolean;
|
|
title: string;
|
|
message: string;
|
|
confirmText: string;
|
|
confirmStyle: 'danger' | 'warning';
|
|
onConfirm: () => void;
|
|
}
|
|
|
|
const PlatformEmailAddressManager: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingAddress, setEditingAddress] = useState<PlatformEmailAddressListItem | null>(null);
|
|
const [confirmModal, setConfirmModal] = useState<ConfirmModalState>({
|
|
isOpen: false,
|
|
title: '',
|
|
message: '',
|
|
confirmText: 'Confirm',
|
|
confirmStyle: 'danger',
|
|
onConfirm: () => {},
|
|
});
|
|
const [formData, setFormData] = useState<EmailAddressFormData>({
|
|
display_name: '',
|
|
sender_name: '',
|
|
assigned_user_id: null,
|
|
local_part: '',
|
|
domain: 'smoothschedule.com',
|
|
color: '#3b82f6',
|
|
password: '',
|
|
is_active: true,
|
|
is_default: false,
|
|
});
|
|
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
|
|
|
const { data: emailAddresses = [], isLoading } = usePlatformEmailAddresses();
|
|
const { data: usersData } = useAssignableUsers();
|
|
const deleteAddress = useDeletePlatformEmailAddress();
|
|
const removeLocal = useRemoveLocalPlatformEmailAddress();
|
|
const testImap = useTestImapConnection();
|
|
const testSmtp = useTestSmtpConnection();
|
|
const syncAddress = useSyncPlatformEmailAddress();
|
|
const setDefault = useSetAsDefault();
|
|
const testMailServer = useTestMailServerConnection();
|
|
const createAddress = useCreatePlatformEmailAddress();
|
|
const updateAddress = useUpdatePlatformEmailAddress();
|
|
const importFromServer = useImportFromMailServer();
|
|
|
|
const handleAdd = () => {
|
|
setEditingAddress(null);
|
|
setFormData({
|
|
display_name: '',
|
|
sender_name: '',
|
|
assigned_user_id: null,
|
|
local_part: '',
|
|
domain: 'smoothschedule.com',
|
|
color: '#3b82f6',
|
|
password: '',
|
|
is_active: true,
|
|
is_default: false,
|
|
});
|
|
setFormErrors({});
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleEdit = (address: PlatformEmailAddressListItem) => {
|
|
setEditingAddress(address);
|
|
setFormData({
|
|
display_name: address.display_name,
|
|
sender_name: address.sender_name || '',
|
|
assigned_user_id: address.assigned_user?.id || null,
|
|
local_part: address.local_part,
|
|
domain: address.domain,
|
|
color: address.color,
|
|
password: '',
|
|
is_active: address.is_active,
|
|
is_default: address.is_default,
|
|
});
|
|
setFormErrors({});
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setIsModalOpen(false);
|
|
setEditingAddress(null);
|
|
setFormErrors({});
|
|
};
|
|
|
|
const validateForm = (): boolean => {
|
|
const errors: Record<string, string> = {};
|
|
|
|
if (!formData.display_name.trim()) {
|
|
errors.display_name = 'Display name is required';
|
|
}
|
|
|
|
if (!editingAddress && !formData.local_part.trim()) {
|
|
errors.local_part = 'Email local part is required';
|
|
} else if (!editingAddress && !/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/i.test(formData.local_part)) {
|
|
errors.local_part = 'Invalid email format';
|
|
}
|
|
|
|
if (!editingAddress && !formData.password) {
|
|
errors.password = 'Password is required';
|
|
} else if (formData.password && formData.password.length < 8) {
|
|
errors.password = 'Password must be at least 8 characters';
|
|
}
|
|
|
|
setFormErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (editingAddress) {
|
|
// Update existing address
|
|
const updateData: any = {
|
|
display_name: formData.display_name,
|
|
sender_name: formData.sender_name,
|
|
assigned_user_id: formData.assigned_user_id,
|
|
color: formData.color,
|
|
is_active: formData.is_active,
|
|
is_default: formData.is_default,
|
|
};
|
|
if (formData.password) {
|
|
updateData.password = formData.password;
|
|
}
|
|
|
|
await updateAddress.mutateAsync({
|
|
id: editingAddress.id,
|
|
data: updateData,
|
|
});
|
|
toast.success('Email address updated successfully');
|
|
} else {
|
|
// Create new address
|
|
await createAddress.mutateAsync({
|
|
display_name: formData.display_name,
|
|
sender_name: formData.sender_name,
|
|
assigned_user_id: formData.assigned_user_id,
|
|
local_part: formData.local_part.toLowerCase(),
|
|
domain: formData.domain,
|
|
color: formData.color,
|
|
password: formData.password,
|
|
is_active: formData.is_active,
|
|
is_default: formData.is_default,
|
|
});
|
|
toast.success('Email address created and synced to mail server');
|
|
}
|
|
handleCloseModal();
|
|
} catch (error: any) {
|
|
const errorMessage =
|
|
error.response?.data?.mail_server ||
|
|
error.response?.data?.local_part ||
|
|
error.response?.data?.detail ||
|
|
'Failed to save email address';
|
|
toast.error(Array.isArray(errorMessage) ? errorMessage[0] : errorMessage);
|
|
}
|
|
};
|
|
|
|
const handleDelete = (id: number, displayName: string) => {
|
|
setConfirmModal({
|
|
isOpen: true,
|
|
title: 'Delete Email Address',
|
|
message: `Are you sure you want to delete "${displayName}"? This will permanently remove the account from both the database and the mail server. This action cannot be undone.`,
|
|
confirmText: 'Delete',
|
|
confirmStyle: 'danger',
|
|
onConfirm: async () => {
|
|
try {
|
|
await deleteAddress.mutateAsync(id);
|
|
toast.success(`${displayName} deleted successfully`);
|
|
} catch (error) {
|
|
toast.error('Failed to delete email address');
|
|
}
|
|
setConfirmModal(prev => ({ ...prev, isOpen: false }));
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleRemoveLocal = (id: number, displayName: string) => {
|
|
setConfirmModal({
|
|
isOpen: true,
|
|
title: 'Remove from Database',
|
|
message: `Remove "${displayName}" from the database? The email account will remain active on the mail server and can be re-imported later.`,
|
|
confirmText: 'Remove',
|
|
confirmStyle: 'warning',
|
|
onConfirm: async () => {
|
|
try {
|
|
const result = await removeLocal.mutateAsync(id);
|
|
toast.success(result.message);
|
|
} catch (error) {
|
|
toast.error('Failed to remove email address');
|
|
}
|
|
setConfirmModal(prev => ({ ...prev, isOpen: false }));
|
|
},
|
|
});
|
|
};
|
|
|
|
const closeConfirmModal = () => {
|
|
setConfirmModal(prev => ({ ...prev, isOpen: false }));
|
|
};
|
|
|
|
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 handleSync = async (id: number, displayName: string) => {
|
|
toast.loading(`Syncing ${displayName} to mail server...`, { id: `sync-${id}` });
|
|
try {
|
|
const result = await syncAddress.mutateAsync(id);
|
|
if (result.success) {
|
|
toast.success(result.message, { id: `sync-${id}` });
|
|
} else {
|
|
toast.error(result.message, { id: `sync-${id}` });
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Sync failed', { id: `sync-${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');
|
|
}
|
|
};
|
|
|
|
const handleTestMailServer = async () => {
|
|
toast.loading('Testing connection to mail server...', { id: 'mail-server-test' });
|
|
try {
|
|
const result = await testMailServer.mutateAsync();
|
|
if (result.success) {
|
|
toast.success(result.message, { id: 'mail-server-test' });
|
|
} else {
|
|
toast.error(result.message, { id: 'mail-server-test' });
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Connection test failed', { id: 'mail-server-test' });
|
|
}
|
|
};
|
|
|
|
const handleImportFromServer = async () => {
|
|
toast.loading('Importing email addresses from mail server...', { id: 'import-emails' });
|
|
try {
|
|
const result = await importFromServer.mutateAsync();
|
|
if (result.success) {
|
|
if (result.imported_count > 0) {
|
|
toast.success(result.message, { id: 'import-emails' });
|
|
} else {
|
|
toast.success('No new email addresses to import', { id: 'import-emails' });
|
|
}
|
|
} else {
|
|
toast.error('Import failed', { id: 'import-emails' });
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Import failed', { id: 'import-emails' });
|
|
}
|
|
};
|
|
|
|
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>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
Email addresses are managed directly on the mail server (mail.talova.net)
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleTestMailServer}
|
|
disabled={testMailServer.isPending}
|
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
<Server className="w-4 h-4" />
|
|
Test Mail Server
|
|
</button>
|
|
<button
|
|
onClick={handleImportFromServer}
|
|
disabled={importFromServer.isPending}
|
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
{importFromServer.isPending ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Download className="w-4 h-4" />
|
|
)}
|
|
Import from Server
|
|
</button>
|
|
<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>
|
|
</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 platform email address to start receiving support tickets
|
|
</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>
|
|
)}
|
|
{address.mail_server_synced ? (
|
|
<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">
|
|
<Server className="w-3 h-3" />
|
|
Synced
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300">
|
|
<AlertTriangle className="w-3 h-3" />
|
|
Not Synced
|
|
</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={() => handleSync(address.id, address.display_name)}
|
|
disabled={syncAddress.isPending}
|
|
className="p-2 text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
|
|
title="Sync to mail server"
|
|
>
|
|
<RefreshCw 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-green-600 dark:text-gray-400 dark:hover:text-green-400 transition-colors"
|
|
title="Test IMAP"
|
|
>
|
|
<TestTube 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={() => handleRemoveLocal(address.id, address.display_name)}
|
|
disabled={removeLocal.isPending}
|
|
className="p-2 text-gray-600 hover:text-orange-600 dark:text-gray-400 dark:hover:text-orange-400 transition-colors"
|
|
title="Remove from database (keep on mail server)"
|
|
>
|
|
<Unlink 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 (also removes from mail server)"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal */}
|
|
{isModalOpen && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg mx-4">
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{editingAddress ? 'Edit Email Address' : 'Add Email Address'}
|
|
</h3>
|
|
<button
|
|
onClick={handleCloseModal}
|
|
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
|
{/* Display Name */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Display Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.display_name}
|
|
onChange={(e) => setFormData({ ...formData, display_name: e.target.value })}
|
|
placeholder="e.g., Support, Billing, Sales"
|
|
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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
/>
|
|
{formErrors.display_name && (
|
|
<p className="mt-1 text-sm text-red-500">{formErrors.display_name}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sender Name */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Sender Name <span className="text-gray-400 font-normal">(optional)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.sender_name}
|
|
onChange={(e) => setFormData({ ...formData, sender_name: e.target.value })}
|
|
placeholder="e.g., SmoothSchedule Support Team"
|
|
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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Name shown in the From field of outgoing emails. If blank, uses Display Name.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Assigned User */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Assigned User <span className="text-gray-400 font-normal">(optional)</span>
|
|
</label>
|
|
<select
|
|
value={formData.assigned_user_id || ''}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
assigned_user_id: e.target.value ? Number(e.target.value) : null
|
|
})}
|
|
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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
>
|
|
<option value="">No user assigned</option>
|
|
{usersData?.users?.map((user) => (
|
|
<option key={user.id} value={user.id}>
|
|
{user.full_name} ({user.email})
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
If assigned, the user's name will be used as the sender name in outgoing emails.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Email Address (only show for new addresses) */}
|
|
{!editingAddress && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Email Address
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
value={formData.local_part}
|
|
onChange={(e) => setFormData({ ...formData, local_part: e.target.value.toLowerCase() })}
|
|
placeholder="support"
|
|
className="flex-1 min-w-0 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
/>
|
|
<span className="flex-shrink-0 text-gray-500 dark:text-gray-400 font-medium">
|
|
@smoothschedule.com
|
|
</span>
|
|
</div>
|
|
{formErrors.local_part && (
|
|
<p className="mt-1 text-sm text-red-500">{formErrors.local_part}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Password */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{editingAddress ? 'New Password (leave blank to keep current)' : 'Password'}
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
placeholder={editingAddress ? 'Leave blank to keep current' : 'Enter password'}
|
|
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 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
/>
|
|
{formErrors.password && (
|
|
<p className="mt-1 text-sm text-red-500">{formErrors.password}</p>
|
|
)}
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Minimum 8 characters. This password will be synced to the mail server.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Color */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Color
|
|
</label>
|
|
<div className="flex gap-2">
|
|
{COLOR_OPTIONS.map((color) => (
|
|
<button
|
|
key={color}
|
|
type="button"
|
|
onClick={() => setFormData({ ...formData, color })}
|
|
className={`w-8 h-8 rounded-full ${
|
|
formData.color === color ? 'ring-2 ring-offset-2 ring-blue-500' : ''
|
|
}`}
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active & Default */}
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.is_active}
|
|
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
|
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">Active</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.is_default}
|
|
onChange={(e) => setFormData({ ...formData, is_default: e.target.checked })}
|
|
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">Default</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-end gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={handleCloseModal}
|
|
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={createAddress.isPending || updateAddress.isPending}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{(createAddress.isPending || updateAddress.isPending) && (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
)}
|
|
{editingAddress ? 'Update' : 'Create'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Confirmation Modal */}
|
|
{confirmModal.isOpen && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
{confirmModal.confirmStyle === 'danger' ? (
|
|
<AlertTriangle className="w-5 h-5 text-red-500" />
|
|
) : (
|
|
<AlertTriangle className="w-5 h-5 text-orange-500" />
|
|
)}
|
|
{confirmModal.title}
|
|
</h3>
|
|
<button
|
|
onClick={closeConfirmModal}
|
|
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
{confirmModal.message}
|
|
</p>
|
|
</div>
|
|
<div className="flex justify-end gap-3 p-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
onClick={closeConfirmModal}
|
|
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={confirmModal.onConfirm}
|
|
disabled={deleteAddress.isPending || removeLocal.isPending}
|
|
className={`px-4 py-2 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 ${
|
|
confirmModal.confirmStyle === 'danger'
|
|
? 'bg-red-600 text-white hover:bg-red-700'
|
|
: 'bg-orange-600 text-white hover:bg-orange-700'
|
|
}`}
|
|
>
|
|
{(deleteAddress.isPending || removeLocal.isPending) && (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
)}
|
|
{confirmModal.confirmText}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PlatformEmailAddressManager;
|