import React, { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Alert, Box, Button, IconButton, LinearProgress, Typography } from "@mui/material"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import CloseIcon from "@mui/icons-material/Close"; import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked"; import { api, getApiErrorMessage } from "../api"; import { useI18n } from "../i18n/I18nProvider"; import { getUserKeyFromToken } from "../themePrefs"; import { PageSection } from "../layout/PageLayout"; type MeResponse = { email?: string | null; firstName?: string | null; lastName?: string | null; displayName?: string | null; }; type CareerProfileResponse = { cvText?: string | null; profile?: { summary?: string[]; jobs?: unknown[]; education?: unknown[]; projects?: unknown[]; certifications?: unknown[]; languages?: unknown[]; otherSections?: unknown[]; skills?: string[]; interests?: string[]; }; }; type ConnectionStatus = { connected?: boolean }; export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | null }) { const navigate = useNavigate(); const { t } = useI18n(); // The session boundary disposes this owner before another account is adopted. const [dismissalKey] = useState(() => `onboardingDismissed:${getUserKeyFromToken()}`); const [dismissed, setDismissed] = useState(() => { try { return window.localStorage.getItem(dismissalKey) === "true"; } catch { return false; } }); const [status, setStatus] = useState<{ profile: boolean; cv: boolean; email: boolean } | null>(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState<{ cause: unknown } | null>(null); const [readAttempt, setReadAttempt] = useState(0); const dismissRef = useRef(null); useEffect(() => { if (dismissed) return; let active = true; setLoading(true); setLoadError(null); setStatus(null); Promise.all([ api.get("/auth/me"), api.get("/career/profile"), Promise.all(["/gmail/status", "/microsoft-graph/status", "/imap/status"].map((url) => api.get(url).then((r) => Boolean(r.data?.connected)), )), ]).then(([me, career, connections]) => { if (!active) return; const profile = career.data?.profile; const structuredCount = [ ...(profile?.summary ?? []), ...(profile?.jobs ?? []), ...(profile?.education ?? []), ...(profile?.projects ?? []), ...(profile?.certifications ?? []), ...(profile?.languages ?? []), ...(profile?.otherSections ?? []), ...(profile?.skills ?? []), ...(profile?.interests ?? []), ].length; setStatus({ profile: Boolean(me.data?.displayName?.trim() || me.data?.firstName?.trim() || me.data?.lastName?.trim()), cv: structuredCount > 0 || Boolean(career.data?.cvText?.trim()), email: connections.some(Boolean), }); }).catch((cause: unknown) => { if (active) setLoadError({ cause }); }).finally(() => { if (active) setLoading(false); }); return () => { active = false; }; }, [dismissed, readAttempt]); if (dismissed) return null; // These four recorded steps are guidance, not a completeness score or proof of // verified email, an imported CV, a rendered PDF, or a live mailbox grant. const steps = [ { done: status?.profile ?? null, label: t("onboardingStepProfile"), path: "/profile", actionLabel: t("onboardingStepProfileAction") }, { done: status?.cv ?? null, label: t("onboardingStepCv"), path: "/career", actionLabel: t("onboardingStepCvAction") }, { done: status?.email ?? null, label: t("onboardingStepEmail"), path: "/settings/connected-accounts", actionLabel: t("onboardingStepEmailAction") }, { done: hasJobs, label: t("onboardingStepJob"), path: "/jobs", actionLabel: t("onboardingStepJobAction") }, ]; const completed = steps.filter((step) => step.done === true).length; if (status && hasJobs !== null && completed === steps.length) return null; return ( { try { window.localStorage.setItem(dismissalKey, "true"); } catch { /* This dismissal still applies while the page remains open. */ } setDismissed(true); }} > }> {loading ? <> {t("loading")} : loadError ? ( { // Retry is replaced by loading feedback; keep focus inside this setup section. dismissRef.current?.focus(); setReadAttempt((attempt) => attempt + 1); }}>{t("retry")} }>{getApiErrorMessage(loadError.cause, t("onboardingLoadFailed"))} ) : status ? <> {hasJobs === null ? ( {t("onboardingJobStatusUnknown")} ) : {completed} / {steps.length}} {steps.map((step) => ( {step.done === null ? null : step.done ? : } {step.label} {step.done === false ? : null} ))} : null} ); }