import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, Button, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Divider, List, ListItemButton, ListItemText, Paper, Tab, Tabs, TextField, ToggleButton, ToggleButtonGroup, Typography, } from "@mui/material"; import { alpha, useTheme } from "@mui/material/styles"; import { api } from "../api"; import { useToast } from "../toast"; import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types"; function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string; } { const lines = raw.replace(/\r\n/g, "\n").split("\n"); const headers: Record = {}; let i = 0; for (; i < lines.length; i++) { const line = lines[i]; if (line.trim() === "") { i++; break; } const idx = line.indexOf(":"); if (idx <= 0) continue; const k = line.slice(0, idx).trim().toLowerCase(); const v = line.slice(idx + 1).trim(); headers[k] = headers[k] ? `${headers[k]} ${v}` : v; } const body = lines.slice(i).join("\n").trim(); const subject = headers["subject"]; const from = headers["from"]; const to = headers["to"]; const dateRaw = headers["date"]; let iso: string | undefined = undefined; if (dateRaw) { const d = new Date(dateRaw); if (!Number.isNaN(+d)) iso = d.toISOString(); } return { subject, from, to, date: iso, body }; } export default function Correspondence({ jobId }: { jobId: number }) { const theme = useTheme(); const { toast } = useToast(); const [messages, setMessages] = useState([]); const [from, setFrom] = useState<"Me" | "Company">("Me"); const [text, setText] = useState(""); const scrollRef = useRef(null); const [importOpen, setImportOpen] = useState(false); const [importTab, setImportTab] = useState(0); const [rawEmail, setRawEmail] = useState(""); const [gmailStatus, setGmailStatus] = useState(null); const [gmailLoading, setGmailLoading] = useState(false); const [gmailQuery, setGmailQuery] = useState(""); const [gmailMessages, setGmailMessages] = useState([]); const [gmailMessagesLoading, setGmailMessagesLoading] = useState(false); const [importingMessageId, setImportingMessageId] = useState(null); const load = useCallback(async () => { const res = await api.get(`/correspondence/${jobId}`); setMessages(res.data); }, [jobId]); const loadGmailStatus = useCallback(async () => { try { setGmailLoading(true); const res = await api.get("/gmail/status"); setGmailStatus(res.data); } catch { setGmailStatus({ connected: false }); } finally { setGmailLoading(false); } }, []); const loadGmailMessages = useCallback(async () => { try { setGmailMessagesLoading(true); const res = await api.get("/gmail/messages", { params: { query: gmailQuery.trim() || undefined, maxResults: 12, }, }); setGmailMessages(res.data); } catch { toast("Failed to load Gmail messages.", "error"); } finally { setGmailMessagesLoading(false); } }, [gmailQuery, toast]); useEffect(() => { void load(); }, [load]); useEffect(() => { const el = scrollRef.current; if (!el) return; el.scrollTop = el.scrollHeight; }, [messages.length]); useEffect(() => { if (!importOpen) return; void loadGmailStatus(); }, [importOpen, loadGmailStatus]); useEffect(() => { if (!importOpen || importTab !== 1 || !gmailStatus?.connected) return; void loadGmailMessages(); }, [importOpen, importTab, gmailStatus?.connected, loadGmailMessages]); useEffect(() => { const onMessage = (event: MessageEvent) => { const data = event.data as { source?: string; status?: string; message?: string }; if (data?.source !== "jobtracker-gmail-oauth") return; if (data.status === "connected") { toast(data.message || "Gmail connected.", "success"); void loadGmailStatus(); setImportTab(1); void loadGmailMessages(); } else { toast(data.message || "Gmail connection failed.", "error"); } }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, [loadGmailMessages, loadGmailStatus, toast]); const canSend = useMemo(() => text.trim().length > 0, [text]); const send = async () => { if (!canSend) return; try { await api.post("/correspondence", { jobApplicationId: jobId, from, content: text, }); setText(""); await load(); } catch { toast("Failed to add message.", "error"); } }; const importEmail = async () => { const parsed = parseRawEmail(rawEmail); if (!parsed.body && !parsed.subject && !rawEmail.trim()) { toast("Paste an email first.", "error"); return; } try { await api.post("/correspondence", { jobApplicationId: jobId, from, channel: "Email", subject: parsed.subject ?? null, content: parsed.body || rawEmail, date: parsed.date ?? null, }); setImportOpen(false); setRawEmail(""); await load(); toast("Email logged.", "success"); } catch { toast("Failed to import email.", "error"); } }; const connectGmail = async () => { try { const res = await api.get<{ url: string }>("/gmail/connect-url"); const popup = window.open(res.data.url, "jobtracker-gmail-connect", "width=620,height=760,resizable=yes,scrollbars=yes"); if (!popup) { toast("Your browser blocked the Gmail popup.", "error"); } } catch { toast("Failed to start Gmail connection.", "error"); } }; const disconnectGmail = async () => { try { await api.delete("/gmail/connection"); setGmailStatus({ connected: false }); setGmailMessages([]); toast("Gmail disconnected.", "success"); } catch { toast("Failed to disconnect Gmail.", "error"); } }; const importGmailMessage = async (messageId: string) => { try { setImportingMessageId(messageId); await api.post("/gmail/import", { jobApplicationId: jobId, messageId, }); await load(); toast("Email imported from Gmail.", "success"); } catch (error: any) { const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to import Gmail message."; toast(message, "error"); } finally { setImportingMessageId(null); } }; return ( {messages.length === 0 ? ( No messages yet. ) : ( {messages.map((m) => { const isMe = (m.from || "").toLowerCase() === "me"; const accent = isMe ? theme.palette.primary.main : theme.palette.warning.main; return ( {m.subject ? ( {m.subject} ) : null} {m.content} {isMe ? "Me" : "Company"} {m.channel ? ` · ${m.channel}` : ""} {m.date ? ` · ${new Date(m.date).toLocaleString()}` : ""} ); })} )} { if (v) setFrom(v); }} size="small" > Me Company