// Email conversations and the existing durable outbound composer. const { useState: useMailState, useEffect: useMailEffect, useRef: useMailRef } = React; const MAIL_STATES = { pending: ["В очереди", "text-[rgb(var(--hd-warning-text))]"], sending: ["Отправляется", "text-[rgb(var(--hd-accent-text))]"], sent: ["Отправлено", "text-[rgb(var(--hd-success-text))]"], failed: ["Не отправлено", "text-[rgb(var(--hd-danger-text))]"], unknown: ["Требует проверки", "text-[rgb(var(--hd-amber-text))]"], }; function EmailComposeScreen({ visible, showToast, currentOperator }) { const [config, setConfig] = useMailState(null); const [letters, setLetters] = useMailState([]); const [sender, setSender] = useMailState(""); const [recipient, setRecipient] = useMailState(""); const [subject, setSubject] = useMailState(""); const [text, setText] = useMailState(""); const [attachment, setAttachment] = useMailState(null); const [busy, setBusy] = useMailState(false); const [error, setError] = useMailState(""); const [loadError, setLoadError] = useMailState(""); const requestKey = useMailRef(null); const busyRef = useMailRef(false); const fileRef = useMailRef(null); const alive = useMailRef(true); useMailEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); useMailEffect(() => { if (!visible) return; let cancelled = false; let loading = false; const refresh = async () => { if (loading) return; loading = true; try { const [settings, history] = await Promise.all([ window.apiFetch("GET", "/api/email/settings", undefined, false, 10000), window.apiFetch("GET", "/api/email/outbox", undefined, false, 10000), ]); if (cancelled) return; setConfig(settings); setLetters(history); setSender(previous => previous || settings.senders.find(item => item.ready)?.address || settings.senders[0]?.address || ""); setLoadError(""); } catch { if (!cancelled) setLoadError("Не удалось загрузить почту. Повторим загрузку автоматически."); } finally { loading = false; } }; refresh(); const timer = setInterval(refresh, 5000); return () => { cancelled = true; clearInterval(timer); }; }, [visible]); function change(setter, value) { setter(value); requestKey.current = null; setError(""); } function selectFile(file) { if (file && file.size > (config?.max_attachment_bytes || 10 * 1024 * 1024)) { setError("Вложение — до 10 МБ"); if (fileRef.current) fileRef.current.value = ""; return; } change(setAttachment, file); if (!file && fileRef.current) fileRef.current.value = ""; } async function submit(event) { event.preventDefault(); if (busyRef.current) return; busyRef.current = true; setBusy(true); setError(""); try { // Reuse after a timeout: the server deduplicates this operator/request. requestKey.current ||= crypto.randomUUID(); const body = new FormData(); for (const [key, value] of Object.entries({ sender, recipient, subject, text, request_key: requestKey.current, })) body.append(key, value); if (attachment) body.append("attachment", attachment); const letter = await window.apiFetch("POST", "/api/email/outbox", body); if (!alive.current) return; setLetters(previous => [letter, ...previous.filter(item => item.id !== letter.id)].slice(0, 100)); setRecipient(""); setSubject(""); setText(""); setAttachment(null); requestKey.current = null; if (fileRef.current) fileRef.current.value = ""; showToast("Письмо поставлено в очередь"); } catch (failure) { if (alive.current) setError(typeof failure.detail === "string" ? failure.detail : "Запрос не подтверждён. Повторите отправку без изменения письма — дубликат не создастся."); } finally { busyRef.current = false; if (alive.current) setBusy(false); } } async function retry(letter) { try { const updated = await window.apiFetch("POST", "/api/email/outbox/" + letter.id + "/retry"); if (alive.current) { setLetters(previous => previous.map(item => item.id === updated.id ? updated : item)); showToast("Отправка поставлена в очередь повторно"); } } catch (failure) { showToast(typeof failure.detail === "string" ? failure.detail : "Не удалось повторить отправку", "error"); } } const ready = config?.enabled && config.senders.some(item => item.address === sender && item.ready); const inputClass = "w-full bg-[rgb(var(--hd-canvas))] border border-[rgb(var(--hd-border))] rounded-lg px-3 py-2 text-sm text-[rgb(var(--hd-text))] focus:outline-none focus:border-[rgb(var(--hd-accent))] disabled:opacity-50"; return (
{loadError &&

{loadError}

} {config && !ready &&

{config.enabled ? "Выбранный адрес пока не подключён. Выберите другой адрес или обратитесь к администратору." : "Почта пока не подключена. Обратитесь к администратору."}

}

Новое письмо