feat(workspace): refine career workflows
Make job/CV comparisons language-aware and filter recruitment noise. Improve responsive career navigation, shared spacing, dashboard priorities, settings, localized workspace controls, and portable browser tests.
This commit is contained in:
@@ -9,6 +9,7 @@ import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
|
||||
import { getApiErrorMessage } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import {
|
||||
CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi,
|
||||
} from "../applicationWorkspace";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
// scheduled...). Everything here is the user's to tick, add to, reorder or dismiss.
|
||||
// docs/architecture/application-workspace.md.
|
||||
export default function ApplicationChecklist({ jobId, onChanged }: { jobId: number; onChanged?: () => void }) {
|
||||
const { t } = useI18n();
|
||||
const [checklist, setChecklist] = useState<Checklist | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -30,9 +32,9 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
setChecklist(await applicationChecklistApi.get(jobId));
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(getApiErrorMessage(err, "Could not load the checklist."));
|
||||
setError(getApiErrorMessage(err, t("checklistLoadFailed")));
|
||||
}
|
||||
}, [jobId]);
|
||||
}, [jobId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -47,11 +49,11 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
await load();
|
||||
onChanged?.();
|
||||
} catch (err) {
|
||||
setError(getApiErrorMessage(err, "Could not update the checklist."));
|
||||
setError(getApiErrorMessage(err, t("checklistUpdateFailed")));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [load, onChanged]);
|
||||
}, [load, onChanged, t]);
|
||||
|
||||
const toggle = (item: ChecklistItem) =>
|
||||
mutate(() => applicationChecklistApi.update(jobId, item.id, {
|
||||
@@ -98,15 +100,15 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
{progress && (
|
||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||
<Stack direction="row" alignItems="baseline" justifyContent="space-between" sx={{ mb: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>Application checklist</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t("checklistTitle")}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{progress.completed} of {progress.total} done
|
||||
{t("checklistProgress", { completed: progress.completed, total: progress.total })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress.percent}
|
||||
aria-label="Checklist completion"
|
||||
aria-label={t("checklistCompletion")}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Paper>
|
||||
@@ -114,9 +116,11 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
|
||||
{grouped.map((group) => (
|
||||
<Paper key={group.key} sx={{ p: 2, borderRadius: 3 }}>
|
||||
<Typography variant="overline" color="text.secondary">{group.label}</Typography>
|
||||
<Typography variant="overline" color="text.secondary">{checklistCategoryLabel(t, group.key)}</Typography>
|
||||
<Stack sx={{ mt: 0.5 }}>
|
||||
{group.items.map((item) => (
|
||||
{group.items.map((item) => {
|
||||
const display = checklistItemDisplay(t, item);
|
||||
return (
|
||||
<Stack
|
||||
key={item.id}
|
||||
direction="row"
|
||||
@@ -129,7 +133,7 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
checked={item.status === "done"}
|
||||
disabled={busy}
|
||||
onChange={() => toggle(item)}
|
||||
inputProps={{ "aria-label": item.title }}
|
||||
inputProps={{ "aria-label": display.title }}
|
||||
sx={{ mt: -0.25 }}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
@@ -142,40 +146,41 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
color: item.status === "done" ? "text.disabled" : "text.primary",
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
{display.title}
|
||||
</Typography>
|
||||
{!item.isSystemGenerated && <Chip size="small" label="Yours" variant="outlined" />}
|
||||
{item.isAutoCompleted && <Chip size="small" label="Detected" color="success" variant="outlined" />}
|
||||
{!item.isSystemGenerated && <Chip size="small" label={t("checklistYours")} variant="outlined" />}
|
||||
{item.isAutoCompleted && <Chip size="small" label={t("checklistDetected")} color="success" variant="outlined" />}
|
||||
</Stack>
|
||||
{item.description && (
|
||||
<Typography variant="caption" color="text.secondary">{item.description}</Typography>
|
||||
{display.description && (
|
||||
<Typography variant="caption" color="text.secondary">{display.description}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Stack direction="row" className="checklist-actions" sx={{ opacity: { xs: 1, md: 0 }, transition: "opacity .15s" }}>
|
||||
<Tooltip title="Move up">
|
||||
<Tooltip title={t("checklistMoveUp")}>
|
||||
<span>
|
||||
<IconButton size="small" disabled={busy} aria-label={`Move up: ${item.title}`} onClick={() => move(item, -1)}>
|
||||
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveUpItem", { title: display.title })} onClick={() => move(item, -1)}>
|
||||
<ArrowUpwardIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Move down">
|
||||
<Tooltip title={t("checklistMoveDown")}>
|
||||
<span>
|
||||
<IconButton size="small" disabled={busy} aria-label={`Move down: ${item.title}`} onClick={() => move(item, 1)}>
|
||||
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveDownItem", { title: display.title })} onClick={() => move(item, 1)}>
|
||||
<ArrowDownwardIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={item.isSystemGenerated ? "Not relevant for this role" : "Delete"}>
|
||||
<Tooltip title={item.isSystemGenerated ? t("checklistNotRelevant") : t("deleteAction")}>
|
||||
<span>
|
||||
<IconButton size="small" disabled={busy} aria-label={`Remove: ${item.title}`} onClick={() => remove(item)}>
|
||||
<IconButton size="small" disabled={busy} aria-label={t("checklistRemoveItem", { title: display.title })} onClick={() => remove(item)}>
|
||||
<DeleteOutlineIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
@@ -185,14 +190,34 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Add your own task"
|
||||
label={t("checklistAddTask")}
|
||||
value={draft}
|
||||
disabled={busy}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" variant="contained" disabled={busy || !draft.trim()}>Add</Button>
|
||||
<Button type="submit" variant="contained" disabled={busy || !draft.trim()}>{t("checklistAdd")}</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function checklistCategoryLabel(t: (key: any, vars?: Record<string, string | number>) => string, category: string) {
|
||||
const keys: Record<string, string> = {
|
||||
preparation: "checklistCategoryPreparation",
|
||||
submission: "checklistCategorySubmission",
|
||||
"follow-up": "checklistCategoryFollowUp",
|
||||
interview: "checklistCategoryInterview",
|
||||
custom: "checklistCategoryCustom",
|
||||
};
|
||||
return keys[category] ? t(keys[category]) : category;
|
||||
}
|
||||
|
||||
function checklistItemDisplay(t: (key: any, vars?: Record<string, string | number>) => string, item: ChecklistItem) {
|
||||
if (!item.systemKey || item.systemKey.startsWith("learning:")) return { title: item.title, description: item.description };
|
||||
const token = item.systemKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
||||
return {
|
||||
title: t(`checklistItem_${token}`),
|
||||
description: t(`checklistItem_${token}Description`),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user