Add OAth flow for Gmail and update tables and UI

This commit is contained in:
cesnimda
2026-03-21 14:02:19 +01:00
parent 51a539068f
commit ed68e44eaf
17 changed files with 1180 additions and 53 deletions
@@ -10,6 +10,7 @@ import {
DialogTitle,
Tab,
Tabs,
TextField,
Typography,
} from "@mui/material";
@@ -55,12 +56,16 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
const [isAdmin, setIsAdmin] = useState(false);
const [followUpDraft, setFollowUpDraft] = useState<FollowUpDraft | null>(null);
const [loadingDraft, setLoadingDraft] = useState(false);
const [sendingDraft, setSendingDraft] = useState(false);
const [draftRecipient, setDraftRecipient] = useState("");
const [draftSubject, setDraftSubject] = useState("");
const [draftBody, setDraftBody] = useState("");
useEffect(() => {
if (!open || !jobId) return;
setTab(0);
setFollowUpDraft(null);
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => setJob(r.data));
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => { setJob(r.data); setDraftRecipient(r.data.company?.recruiterEmail ?? ""); });
api
.get(`/auth/me`)
.then((r) => setIsAdmin(Boolean(r.data?.roles?.includes("Admin"))))
@@ -76,7 +81,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
setLoadingDraft(true);
api
.get<FollowUpDraft>(`/jobapplications/${jobId}/followup-draft`)
.then((r) => setFollowUpDraft(r.data))
.then((r) => { setFollowUpDraft(r.data); setDraftSubject(r.data.subject); setDraftBody(r.data.body); })
.catch(() => setFollowUpDraft(null))
.finally(() => setLoadingDraft(false));
}, [open, jobId, tab, followUpDraft]);
@@ -216,17 +221,31 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
<Typography variant="overline">Suggested send date</Typography>
<Typography>{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()}</Typography>
</Box>
<Box>
<Typography variant="overline">Subject</Typography>
<Typography sx={{ fontWeight: 700 }}>{followUpDraft.subject}</Typography>
</Box>
<Box>
<Typography variant="overline">Draft</Typography>
<Typography sx={{ whiteSpace: "pre-wrap" }}>{followUpDraft.body}</Typography>
</Box>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button variant="contained" onClick={() => navigator.clipboard.writeText(`${followUpDraft.subject}\n\n${followUpDraft.body}`)}>
Copy draft
<TextField label="Recipient" value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText="Defaults to the company recruiter email when available." />
<TextField label="Subject" value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
<TextField label="Draft" multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} />
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
<Button variant="outlined" onClick={() => navigator.clipboard.writeText(`${draftSubject}\n\n${draftBody}`)}>Copy draft</Button>
<Button
variant="contained"
disabled={sendingDraft || !draftSubject.trim() || !draftBody.trim()}
onClick={async () => {
if (!jobId) return;
setSendingDraft(true);
try {
await api.post(`/jobapplications/${jobId}/send-followup`, {
toEmail: draftRecipient || null,
subject: draftSubject,
body: draftBody,
nextFollowUpAt: followUpDraft.suggestedSendOn || null,
});
setJob((prev) => prev ? { ...prev, followUpAt: followUpDraft.suggestedSendOn } : prev);
} finally {
setSendingDraft(false);
}
}}
>
{sendingDraft ? "Sending..." : "Send and log email"}
</Button>
</Box>
</Box>