import React, { useEffect, useMemo, useState } from "react"; import { Box, Button, Chip, CircularProgress, Dialog, DialogContent, DialogTitle, FormControl, InputLabel, MenuItem, Select, Tab, Tabs, TextField, Typography, } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; import { ApplicationPackageResponse, CandidateFit, InterviewPrepResponse, JobApplication, ReadinessResponse } from "../types"; import { useToast } from "../toast"; import { useDialogActions } from "../dialogs"; import Correspondence from "./Correspondence"; import Attachments from "./Attachments"; import JobFlowBar from "./JobFlowBar"; import { useI18n } from "../i18n/I18nProvider"; type FollowUpDraft = { subject: string; body: string; reason: string; suggestedSendOn: string; }; type GenerationMode = "default" | "concise" | "ats" | "achievement" | "interview"; interface Props { open: boolean; jobId: number | null; onClose: () => void; initialTab?: number; } function statusChipColor(status: string): "default" | "primary" | "warning" | "error" | "success" { switch (status) { case "Rejected": return "error"; case "Waiting": case "Ghosted": return "warning"; case "Offer": return "success"; case "Applied": default: return "primary"; } } function getFitLevel(candidateFit: CandidateFit | null): { label: string; color: "success" | "warning" | "default" } | null { if (!candidateFit) return null; if (candidateFit.fitLevel === "Strong match") return { label: candidateFit.fitLevel, color: "success" }; if (candidateFit.fitLevel === "Potential match") return { label: candidateFit.fitLevel, color: "warning" }; return { label: candidateFit.fitLevel, color: "default" }; } function copyLines(items: string[]) { return navigator.clipboard.writeText(items.map((item) => `• ${item}`).join("\n")); } export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0 }: Props) { const { toast } = useToast(); const { t } = useI18n(); const { confirmAction } = useDialogActions(); const [job, setJob] = useState(null); const [tab, setTab] = useState(0); const [history, setHistory] = useState<{ id: number; type: string; oldValue?: string; newValue?: string; note?: string; at: string }[]>([]); const [isAdmin, setIsAdmin] = useState(false); const [followUpDraft, setFollowUpDraft] = useState(null); const [loadingDraft, setLoadingDraft] = useState(false); const [sendingDraft, setSendingDraft] = useState(false); const [refreshingAi, setRefreshingAi] = useState(false); const [candidateFit, setCandidateFit] = useState(null); const [loadingCandidateFit, setLoadingCandidateFit] = useState(false); const [interviewPrep, setInterviewPrep] = useState(null); const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false); const [readiness, setReadiness] = useState(null); const [loadingReadiness, setLoadingReadiness] = useState(false); const [savingTailoredCv, setSavingTailoredCv] = useState(false); const [savingApplicationDrafts, setSavingApplicationDrafts] = useState(false); const [generatingPackage, setGeneratingPackage] = useState(false); const [applicationPackage, setApplicationPackage] = useState(null); const [generationMode, setGenerationMode] = useState("default"); const [tailoredCvText, setTailoredCvText] = useState(""); const [draftRecipient, setDraftRecipient] = useState(""); const [draftSubject, setDraftSubject] = useState(""); const [draftBody, setDraftBody] = useState(""); useEffect(() => { if (!open || !jobId) return; setTab(Math.max(0, Math.min(8, initialTab))); setFollowUpDraft(null); setCandidateFit(null); setInterviewPrep(null); setReadiness(null); setApplicationPackage(null); api.get(`/jobapplications/${jobId}`).then((r) => { setJob(r.data); setTailoredCvText(r.data.tailoredCvText ?? ""); setDraftRecipient(r.data.company?.recruiterEmail ?? ""); }); api.get(`/auth/me`).then((r) => setIsAdmin(Boolean(r.data?.roles?.includes("Admin")))).catch(() => setIsAdmin(false)); api.get(`/jobapplications/${jobId}/history`).then((r) => setHistory(r.data)).catch(() => setHistory([])); }, [open, jobId, initialTab]); useEffect(() => { if (!open || !jobId || tab !== 4 || followUpDraft) return; setLoadingDraft(true); api.get(`/jobapplications/${jobId}/followup-draft`).then((r) => { setFollowUpDraft(r.data); setDraftSubject(r.data.subject); setDraftBody(r.data.body); }).catch(() => setFollowUpDraft(null)).finally(() => setLoadingDraft(false)); }, [open, jobId, tab, followUpDraft]); useEffect(() => { if (!open || !jobId || tab !== 5 || candidateFit) return; setLoadingCandidateFit(true); api.get(`/jobapplications/${jobId}/candidate-fit`).then((r) => setCandidateFit(r.data)).catch(() => setCandidateFit(null)).finally(() => setLoadingCandidateFit(false)); }, [open, jobId, tab, candidateFit]); useEffect(() => { if (!open || !jobId || tab !== 6 || interviewPrep) return; setLoadingInterviewPrep(true); api.get(`/jobapplications/${jobId}/interview-prep`).then((r) => setInterviewPrep(r.data)).catch(() => setInterviewPrep(null)).finally(() => setLoadingInterviewPrep(false)); }, [open, jobId, tab, interviewPrep]); useEffect(() => { if (!open || !jobId || tab !== 7 || readiness) return; setLoadingReadiness(true); api.get(`/jobapplications/${jobId}/readiness`).then((r) => setReadiness(r.data)).catch(() => setReadiness(null)).finally(() => setLoadingReadiness(false)); }, [open, jobId, tab, readiness]); const tags: string[] = (() => { const raw = job?.tags; if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []; } catch { return []; } })(); const title = job ? `${job.company?.name ?? ""} - ${job.jobTitle}` : t("addJobApplication"); const checklist = [ job?.hasResume ? t("jobDetailsResume") : null, job?.hasCoverLetter ? t("jobDetailsCoverLetter") : null, job?.hasPortfolio ? t("jobDetailsPortfolio") : null, job?.hasOtherAttachment ? t("jobDetailsOther") : null, ].filter(Boolean).join(", ") || t("jobDetailsNotAvailable"); const summaryFirstText = job?.fullSummary ?? job?.shortSummary ?? t("jobTableNoSummaryYet"); const translatedDescriptionText = job?.translatedDescription?.trim() || ""; const originalDescriptionText = job?.description?.trim() || ""; const showTranslatedText = translatedDescriptionText.length > 0; const showOriginalText = originalDescriptionText.length > 0; const fitLevel = useMemo(() => getFitLevel(candidateFit), [candidateFit]); return ( {t("jobTableOpen")} {title} {job && } {summaryFirstText} setTab(v)} sx={{ mb: 2 }} variant="scrollable" allowScrollButtonsMobile> {isAdmin ? : null} {tab === 0 && ( {t("jobDetailsDateApplied")}{job ? new Date(job.dateApplied).toLocaleDateString() : ""} {t("jobDetailsDaysSince")}{job?.daysSince ?? ""} {t("jobTableLocation")}{job?.location ?? ""} {t("jobDetailsSalary")}{job?.salary ?? ""} {t("jobDetailsNextAction")}{job?.nextAction ?? ""} {t("jobDetailsFollowUp")}{job?.followUpAt ? new Date(job.followUpAt).toLocaleDateString() : ""} {t("jobDetailsDeadline")}{job?.deadline ? new Date(job.deadline).toLocaleDateString() : ""} {t("jobDetailsTags")}{tags.length === 0 ? - : tags.map((t) => )} {t("jobDetailsAttachmentTypes")}{checklist} {t("jobDetailsJobUrl")}{job?.jobUrl ? {job.jobUrl} : ""} {t("jobDetailsSummaryAndSkills")} {summaryFirstText} {showTranslatedText ? ( {t("jobDetailsTranslatedRoleText")} {translatedDescriptionText} ) : null} {showOriginalText ? ( {t("jobDetailsOriginalRoleText")} {originalDescriptionText} ) : null} {t("editJobNotes")}{job?.notes ?? ""} )} {tab === 1 && jobId && } {tab === 2 && jobId && } {tab === 3 && ( {t("jobDetailsTabTailoredCv")} {t("jobDetailsTailoredCvMode")} {t("jobDetailsTailoredCvIntro")} setTailoredCvText(e.target.value)} multiline minRows={14} fullWidth placeholder={t("jobDetailsTailoredCvPlaceholder")} /> {t("jobDetailsLastUpdated", { value: job?.tailoredCvUpdatedAt ? new Date(job.tailoredCvUpdatedAt).toLocaleString() : t("jobDetailsNotSavedYet") })} {applicationPackage ? ( { if (!jobId) return; setSavingApplicationDrafts(true); try { await api.put(`/jobapplications/${jobId}/application-drafts`, { coverLetterText: content }); setJob((prev) => prev ? { ...prev, coverLetterText: content } : prev); setReadiness(null); toast(t("jobDetailsCoverLetterSaved"), "success"); } catch (error: any) { toast(getApiErrorMessage(error, t("jobDetailsCoverLetterSaveFailed")), "error"); } finally { setSavingApplicationDrafts(false); } }} saving={savingApplicationDrafts} /> { if (!jobId) return; setSavingApplicationDrafts(true); try { await api.put(`/jobapplications/${jobId}/application-drafts`, { notes: `Application answer draft:\n${content}` }); setReadiness(null); toast(t("jobDetailsApplicationAnswerSaved"), "success"); } catch (error: any) { toast(getApiErrorMessage(error, t("jobDetailsApplicationAnswerSaveFailed")), "error"); } finally { setSavingApplicationDrafts(false); } }} saving={savingApplicationDrafts} /> { if (!jobId) return; setSavingApplicationDrafts(true); try { await api.put(`/jobapplications/${jobId}/application-drafts`, { recruiterMessageDraft: content }); setJob((prev) => prev ? { ...prev, recruiterMessageDraft: content } : prev); toast(t("jobDetailsRecruiterMessageSaved"), "success"); } catch (error: any) { toast(getApiErrorMessage(error, t("jobDetailsRecruiterMessageSaveFailed")), "error"); } finally { setSavingApplicationDrafts(false); } }} saving={savingApplicationDrafts} /> ) : null} )} {tab === 4 && ( {loadingDraft ? : followUpDraft ? ( {t("jobDetailsReason")}{followUpDraft.reason} {t("jobDetailsSuggestedSendDate")}{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()} setDraftRecipient(e.target.value)} helperText={t("jobDetailsRecipientHelp")} /> setDraftSubject(e.target.value)} /> setDraftBody(e.target.value)} /> ) : {t("jobDetailsNoDraftAvailable")}} )} {tab === 5 && ( {loadingCandidateFit ? : candidateFit ? ( {t("jobDetailsHowYouMatch")}{candidateFit.matchSummary} = 75 ? "success" : candidateFit.matchScore >= 55 ? "warning" : "default"} size="small" /> {fitLevel ? : null} ) : {t("jobDetailsCandidateFitEmpty")}} )} {tab === 6 && ( {loadingInterviewPrep ? : interviewPrep ? ( ) : {t("jobDetailsNoInterviewPrep")}} )} {tab === 7 && ( {loadingReadiness ? : readiness ? ( {t("jobDetailsApplicationReadiness")} = 80 ? "success" : readiness.score >= 60 ? "warning" : "default"} /> ) : {t("jobDetailsNoReadiness")}} )} {tab === 8 && isAdmin && ( {history.length === 0 ? {t("jobDetailsNoHistory")} : history.map((entry) => )} )} ); } function SectionChips({ title, items, color, outlined }: { title: string; items: string[]; color: "success" | "warning"; outlined?: boolean }) { const { t } = useI18n(); return ( {title} {items.length ? items.map((item) => ) : {t("jobDetailsNothingHighlighted")}} ); } function TwoColumnSection({ leftTitle, leftItems, rightTitle, rightItems }: { leftTitle: string; leftItems: string[]; rightTitle: string; rightItems: string[] }) { return ( ); } function ListCard({ title, items }: { title: string; items: string[] }) { const { t } = useI18n(); return ( {title} {items.length ? items.map((item, index) => • {item}) : {t("jobDetailsNothingHighlighted")}} ); } function DraftCard({ title, content, onSave, saving }: { title: string; content: string; onSave?: (content: string) => Promise | void; saving?: boolean }) { const { t } = useI18n(); const [value, setValue] = React.useState(content); React.useEffect(() => { setValue(content); }, [content]); return ( {title} {onSave ? : null} setValue(e.target.value)} multiline minRows={6} fullWidth /> ); } function PaperRow({ type, oldValue, newValue, at, note }: { type: string; oldValue?: string; newValue?: string; at: string; note?: string }) { return ( {type} {oldValue || newValue ? {" "}({oldValue ?? ""} {oldValue || newValue ? "->" : ""} {newValue ?? ""}) : null} {at ? new Date(at).toLocaleString() : ""} {note ? ` - ${note}` : ""} ); }