118 lines
6.1 KiB
TypeScript
118 lines
6.1 KiB
TypeScript
import React from "react";
|
|
import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Chip, LinearProgress, Typography } from "@mui/material";
|
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
import { useI18n } from "../../i18n/I18nProvider";
|
|
import { localizeCompletenessLabel } from "./careerCompletenessLabels";
|
|
import { PageSection } from "../../layout/PageLayout";
|
|
|
|
// Display-only slice of the Career Profile page: the completeness meter, the "missing" chips, and the
|
|
// version-history accordion. Extracted from CareerProfilePage as the first step of the Phase 1
|
|
// component split. It owns no state and makes no API calls — the parent still holds `completeness`
|
|
// and `versions` and performs the restore — so save/load behaviour is unchanged.
|
|
export type CareerCompletenessView = {
|
|
percent: number;
|
|
missing: string[];
|
|
};
|
|
|
|
export type CareerVersionView = {
|
|
version: number;
|
|
source: string;
|
|
createdAtUtc: string;
|
|
isCurrent: boolean;
|
|
};
|
|
|
|
export default function ProfileCompleteness({
|
|
completeness,
|
|
versions,
|
|
loading,
|
|
onRestore,
|
|
showSummary = true,
|
|
versionsLoading = false,
|
|
versionsError = null,
|
|
onRetryVersions,
|
|
restoreDisabled = false,
|
|
restoreError = null,
|
|
}: {
|
|
completeness: CareerCompletenessView | null;
|
|
versions: CareerVersionView[];
|
|
loading: boolean;
|
|
onRestore: (version: number) => void;
|
|
showSummary?: boolean;
|
|
versionsLoading?: boolean;
|
|
versionsError?: string | null;
|
|
onRetryVersions?: () => void;
|
|
restoreDisabled?: boolean;
|
|
restoreError?: string | null;
|
|
}) {
|
|
const { language, t } = useI18n();
|
|
const restoreHelpId = React.useId();
|
|
const historyRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
const locale = language === "nb" ? "nb-NO" : "en";
|
|
const rows = versions.slice(0, 12).map((v) => (
|
|
<Box key={v.version} sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, flexWrap: "wrap", minWidth: 0 }}>
|
|
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>
|
|
v{v.version} · {v.source} · {new Date(v.createdAtUtc).toLocaleString(locale)}{v.isCurrent ? ` · ${t("profileCvVersionCurrent")}` : ""}
|
|
</Typography>
|
|
{!v.isCurrent ? (
|
|
<Button size="small" variant="text" disabled={loading || versionsLoading || restoreDisabled} aria-describedby={restoreDisabled ? restoreHelpId : undefined} onClick={() => { historyRef.current?.focus(); onRestore(v.version); }}>{t("careerRestoreVersion", { version: v.version })}</Button>
|
|
) : null}
|
|
</Box>
|
|
));
|
|
|
|
return (
|
|
<Box ref={historyRef} role="group" aria-label={t("profileCvVersionHistory", { count: versions.length })} tabIndex={-1} sx={{ mb: 2 }}>
|
|
{showSummary && completeness ? (
|
|
<>
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 2, flexWrap: "wrap", mb: 1 }}>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 900 }}>{t("profileCvCompletenessTitle")}</Typography>
|
|
<Typography variant="h6" sx={{ fontWeight: 900 }}>{completeness.percent}%</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
aria-label={t("profileCvCompletenessTitle")}
|
|
variant="determinate"
|
|
value={completeness.percent}
|
|
color={completeness.percent >= 80 ? "success" : completeness.percent >= 40 ? "primary" : "warning"}
|
|
sx={{ height: 8, borderRadius: 999 }}
|
|
/>
|
|
{completeness.missing.length > 0 ? (
|
|
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", alignItems: "center", mt: 1.25 }}>
|
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvCompletenessMissing")}</Typography>
|
|
{completeness.missing.map((label) => (
|
|
<Chip key={label} size="small" label={localizeCompletenessLabel(label, t)} sx={{ height: 22, fontWeight: 700 }} />
|
|
))}
|
|
</Box>
|
|
) : (
|
|
<Typography variant="body2" sx={{ color: "success.main", mt: 1.25, fontWeight: 700 }}>{t("profileCvCompletenessComplete")}</Typography>
|
|
)}
|
|
</>
|
|
) : null}
|
|
<PageSection title={t("profileCvVersionHistory", { count: versions.length })} surface>
|
|
{versionsLoading ? <LinearProgress aria-label={t("careerHistoryLoading")} /> : null}
|
|
{versionsError !== null ? <Alert severity="error" action={onRetryVersions ? <Button color="inherit" disabled={versionsLoading} onClick={() => { historyRef.current?.focus(); onRetryVersions(); }}>{t("retry")}</Button> : undefined}>
|
|
<Typography component="p" variant="body2" sx={{ fontWeight: 600 }}>{t("careerHistoryLoadFailed")}</Typography>
|
|
{versionsError ? <Typography component="p" variant="body2" sx={{ overflowWrap: "anywhere" }}>{versionsError}</Typography> : null}
|
|
</Alert> : null}
|
|
{restoreError !== null ? <Alert severity="error">
|
|
<Typography component="p" variant="body2" sx={{ fontWeight: 600 }}>{t("careerRestoreFailed")}</Typography>
|
|
{restoreError ? <Typography component="p" variant="body2" sx={{ overflowWrap: "anywhere" }}>{restoreError}</Typography> : null}
|
|
</Alert> : null}
|
|
{restoreDisabled ? <Typography id={restoreHelpId} variant="body2" color="text.secondary">{t("careerSaveBeforeRestore")}</Typography> : null}
|
|
{versions.length > 1 ? (
|
|
<Accordion disableGutters elevation={0} sx={{ mt: showSummary ? 1.5 : 0, "&:before": { display: "none" }, backgroundColor: "transparent" }}>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0, minHeight: 0 }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 700 }}>{t("profileCvVersionHistory", { count: versions.length })}</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails sx={{ px: 0, pt: 0 }}>
|
|
<Box sx={{ display: "grid", gap: 0.75 }}>
|
|
{rows}
|
|
</Box>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
) : rows}
|
|
{versions.length === 0 && !versionsLoading && versionsError === null ? <Typography variant="body2" color="text.secondary">{t("careerHistoryEmpty")}</Typography> : null}
|
|
</PageSection>
|
|
</Box>
|
|
);
|
|
}
|