/** * Email Composer Component * * Compose, reply, and forward emails with rich text editing. */ import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { X, Send, Paperclip, Trash2, Minimize2, Maximize2, Bold, Italic, Underline, List, ListOrdered, Link, Loader2, } from 'lucide-react'; import { StaffEmail, StaffEmailCreateDraft } from '../../types'; import { useCreateDraft, useUpdateDraft, useSendEmail, useUploadAttachment, useContactSearch, useUserEmailAddresses, } from '../../hooks/useStaffEmail'; import toast from 'react-hot-toast'; interface EmailComposerProps { replyTo?: StaffEmail | null; forwardFrom?: StaffEmail | null; onClose: () => void; onSent: () => void; } const EmailComposer: React.FC = ({ replyTo, forwardFrom, onClose, onSent, }) => { const { t } = useTranslation(); const textareaRef = useRef(null); // Get available email addresses for sending (only those assigned to current user) const { data: userEmailAddresses = [] } = useUserEmailAddresses(); // Form state const [fromAddressId, setFromAddressId] = useState(null); const [to, setTo] = useState(''); const [cc, setCc] = useState(''); const [bcc, setBcc] = useState(''); const [subject, setSubject] = useState(''); const [body, setBody] = useState(''); const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [draftId, setDraftId] = useState(null); // Contact search const [toQuery, setToQuery] = useState(''); const { data: contactSuggestions = [] } = useContactSearch(toQuery); // Mutations const createDraft = useCreateDraft(); const updateDraft = useUpdateDraft(); const sendEmail = useSendEmail(); const uploadAttachment = useUploadAttachment(); // Initialize form for reply/forward useEffect(() => { if (replyTo) { // Reply mode setTo(replyTo.fromAddress); setSubject(replyTo.subject.startsWith('Re:') ? replyTo.subject : `Re: ${replyTo.subject}`); setBody(`\n\n---\nOn ${new Date(replyTo.emailDate).toLocaleString()}, ${replyTo.fromName || replyTo.fromAddress} wrote:\n\n${replyTo.bodyText}`); } else if (forwardFrom) { // Forward mode setSubject(forwardFrom.subject.startsWith('Fwd:') ? forwardFrom.subject : `Fwd: ${forwardFrom.subject}`); setBody(`\n\n---\nForwarded message:\nFrom: ${forwardFrom.fromName || forwardFrom.fromAddress} <${forwardFrom.fromAddress}>\nDate: ${new Date(forwardFrom.emailDate).toLocaleString()}\nSubject: ${forwardFrom.subject}\nTo: ${forwardFrom.toAddresses.join(', ')}\n\n${forwardFrom.bodyText}`); } }, [replyTo, forwardFrom]); // Set default from address useEffect(() => { if (!fromAddressId && userEmailAddresses.length > 0) { setFromAddressId(userEmailAddresses[0].id); } }, [userEmailAddresses, fromAddressId]); const parseAddresses = (input: string): string[] => { return input .split(/[,;]/) .map((addr) => addr.trim()) .filter((addr) => addr.length > 0); }; const handleSend = async () => { if (!fromAddressId) { toast.error('Please select a From address'); return; } const toAddresses = parseAddresses(to); if (toAddresses.length === 0) { toast.error('Please enter at least one recipient'); return; } try { // Create or update draft first let emailId = draftId; const draftData: StaffEmailCreateDraft = { emailAddressId: fromAddressId, toAddresses, ccAddresses: parseAddresses(cc), bccAddresses: parseAddresses(bcc), subject: subject || '(No Subject)', bodyText: body, bodyHtml: `
${body.replace(//g, '>')}
`, inReplyTo: replyTo?.id, threadId: replyTo?.threadId || undefined, }; if (emailId) { await updateDraft.mutateAsync({ id: emailId, data: draftData }); } else { const draft = await createDraft.mutateAsync(draftData); emailId = draft.id; setDraftId(emailId); } // Send the email await sendEmail.mutateAsync(emailId); toast.success('Email sent'); onSent(); } catch (error: any) { toast.error(error.response?.data?.error || 'Failed to send email'); } }; const handleSaveDraft = async () => { if (!fromAddressId) { toast.error('Please select a From address'); return; } try { const draftData: StaffEmailCreateDraft = { emailAddressId: fromAddressId, toAddresses: parseAddresses(to), ccAddresses: parseAddresses(cc), bccAddresses: parseAddresses(bcc), subject: subject || '(No Subject)', bodyText: body, bodyHtml: `
${body.replace(//g, '>')}
`, inReplyTo: replyTo?.id, threadId: replyTo?.threadId || undefined, }; if (draftId) { await updateDraft.mutateAsync({ id: draftId, data: draftData }); } else { const draft = await createDraft.mutateAsync(draftData); setDraftId(draft.id); } toast.success('Draft saved'); } catch (error: any) { toast.error(error.response?.data?.error || 'Failed to save draft'); } }; const handleFileUpload = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; // TODO: Implement attachment upload when draft is created toast.error('Attachments not yet implemented'); }; const isSending = createDraft.isPending || updateDraft.isPending || sendEmail.isPending; if (isMinimized) { return (
setIsMinimized(false)} > {subject || 'New Message'}
); } return (
{/* Header */}
{replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'}
{/* Form */}
{/* From */}
{/* To */}
setTo(e.target.value)} placeholder="recipient@example.com" className="flex-1 bg-transparent border-0 text-sm text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 p-0" />
{!showCc && ( )} {!showBcc && ( )}
{/* Cc */} {showCc && (
setCc(e.target.value)} placeholder="cc@example.com" className="flex-1 bg-transparent border-0 text-sm text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 p-0" />
)} {/* Bcc */} {showBcc && (
setBcc(e.target.value)} placeholder="bcc@example.com" className="flex-1 bg-transparent border-0 text-sm text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 p-0" />
)} {/* Subject */}
setSubject(e.target.value)} placeholder="Email subject" className="flex-1 bg-transparent border-0 text-sm text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 p-0" />
{/* Body */}