import React, { useEffect, useMemo, useRef, useState } from "react"; import { Alert, Box, Button, Chip, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material"; import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import PersonOutlineIcon from "@mui/icons-material/PersonOutline"; import UploadFileOutlinedIcon from "@mui/icons-material/UploadFileOutlined"; import WorkOutlineIcon from "@mui/icons-material/WorkOutline"; import { CvVariantSummary, cvBuilderApi } from "../../cvBuilder"; import type { UserOperation } from "../../types"; import { useI18n } from "../../i18n/I18nProvider"; import { PageSection } from "../../layout/PageLayout"; import { localizeCompletenessLabel } from "./careerCompletenessLabels"; export type CareerWorkspaceCompleteness = { percent: number; missing: string[]; }; export type CareerWorkspaceImportRun = { id: number; status: string; errorMessage?: string; operation?: UserOperation | null; }; const activeStatuses = new Set(["queued", "running", "waiting_for_retry", "waiting_for_external_fallback"]); function isActive(run: CareerWorkspaceImportRun) { return run.operation ? activeStatuses.has(run.operation.status) : run.status === "queued" || run.status === "running"; } function ActionCard({ title, body, href, label, icon, onNavigate }: { title: string; body: string; href: string; label: string; icon: React.ReactNode; onNavigate?: () => void; }) { return ( {icon} {title} {body} ); } export default function CareerWorkspaceOverview({ completeness, runs, loading, loadError, onNavigate, runsLoading = false, runsLoadError = null, onRetryProfile, onRetryRuns, profileDirty = false }: { completeness: CareerWorkspaceCompleteness | null; runs: CareerWorkspaceImportRun[]; loading: boolean; loadError: string | null; onNavigate?: (section: "profile" | "import") => void; runsLoading?: boolean; runsLoadError?: string | null; onRetryProfile?: () => void; onRetryRuns?: () => void; profileDirty?: boolean; }) { const { language, t } = useI18n(); const [recentCvs, setRecentCvs] = useState([]); const [recentLoading, setRecentLoading] = useState(true); const [recentError, setRecentError] = useState(false); const [recentAttempt, setRecentAttempt] = useState(0); const profileLinkRef = useRef(null); const importLinkRef = useRef(null); const recentLinkRef = useRef(null); useEffect(() => { let cancelled = false; setRecentLoading(true); cvBuilderApi.list() .then((items) => { if (!cancelled) { setRecentCvs(Array.isArray(items) ? [...items].sort((left, right) => Date.parse(right.updatedAtUtc) - Date.parse(left.updatedAtUtc)).slice(0, 3) : []); setRecentError(false); } }) .catch(() => { if (!cancelled) setRecentError(true); }) .finally(() => { if (!cancelled) setRecentLoading(false); }); return () => { cancelled = true; }; }, [recentAttempt]); const importState = useMemo(() => { const review = runs.filter((run) => run.status === "pending_review"); const active = runs.filter(isActive); const failed = runs.filter((run) => run.status === "failed" || run.operation?.status === "failed"); if (review.length > 0) return { severity: "warning" as const, label: t("careerOverviewReviewImport"), message: t(review.length === 1 ? "careerOverviewReviewOne" : "careerOverviewReviewMany", { count: review.length }) }; if (active.length > 0) return { severity: "info" as const, label: t("careerOverviewViewProcessing"), message: t(active.length === 1 ? "careerOverviewProcessingOne" : "careerOverviewProcessingMany", { count: active.length }) }; if (failed.length > 0) return { severity: "error" as const, label: t("careerOverviewResolveImport"), message: t(failed.length === 1 ? "careerOverviewFailedOne" : "careerOverviewFailedMany", { count: failed.length }) }; return { severity: "success" as const, label: t("careerOverviewImportCv"), message: t(runs.length > 0 ? "careerOverviewImportComplete" : "careerOverviewNoImport") }; }, [runs, t]); const profileKnown = !loading && !loadError && completeness !== null; const importUnknown = runsLoading && runs.length === 0; const importLabel = importUnknown || runsLoadError ? t("careerWorkspaceImportTab") : importState.label; const isFirstRun = profileKnown && !runsLoading && !runsLoadError && !recentLoading && !recentError && completeness.percent === 0 && runs.length === 0 && recentCvs.length === 0; return ( } href="/career/builder">{t("careerOverviewOpenBuilder")} }> {isFirstRun ? {t("careerOverviewFirstRun")} : null} {t("careerOverviewCompleteness")} {loading ? : profileKnown ? {completeness.percent}% : null} {loading || profileKnown ? : null} {loadError ? { profileLinkRef.current?.focus(); onRetryProfile(); }}>{t("retry")} : undefined}> {t("careerOverviewUnavailable")} {profileDirty ? {t("careerOverviewRetryDraftHint")} : null} : null} {profileKnown && completeness.missing.length ? ( {t("careerOverviewAddNext", { items: completeness.missing.slice(0, 3).map((label) => localizeCompletenessLabel(label, t)).join(", ") })} ) : null} {importUnknown ? {t("loading")} : runsLoadError ? { importLinkRef.current?.focus(); onRetryRuns(); }}>{t("retry")} : undefined}>{t("careerOverviewImportUnavailable")} : {importState.message}} onNavigate("profile") : undefined} label={t("careerOverviewEditProfile")} icon={} /> onNavigate("import") : undefined} label={importLabel} icon={} /> } /> } /> {t("careerOverviewViewAll")}}> {recentLoading ? ( {t("loading")} ) : recentError ? ( { recentLinkRef.current?.focus(); setRecentAttempt((attempt) => attempt + 1); }}>{t("retry")}}>{t("careerOverviewRecentFailed")} ) : recentCvs.length === 0 ? ( {t("careerOverviewNoCvs")} ) : ( {recentCvs.map((cv) => ( {cv.name} {t("careerOverviewUpdated", { date: new Date(cv.updatedAtUtc).toLocaleDateString(language === "nb" ? "nb-NO" : "en-GB") })} {cv.jobApplicationId ? : null} ))} )} ); }