feat(S04/T02): Made the job table show actionable follow-up/package nex…
- job-tracker-ui/src/components/JobTable.tsx - job-tracker-ui/src/daily-control-loop.test.tsx - job-tracker-ui/src/i18n/translations.ts - .gsd/milestones/M001/slices/S04/S04-PLAN.md - .gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md
This commit is contained in:
@@ -289,6 +289,59 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
return src.length > 220 ? `${src.slice(0, 220)}...` : src;
|
||||
};
|
||||
|
||||
const openFollowUpWorkspace = (jobId: number) => {
|
||||
navigate(buildJobWorkspacePath(jobId, { tab: JOB_DETAILS_TABS.followUp, followMode: "waiting-update" }));
|
||||
};
|
||||
|
||||
const openTailoredCvWorkspace = (jobId: number) => {
|
||||
navigate(buildJobWorkspacePath(jobId, { tab: JOB_DETAILS_TABS.tailoredCv }));
|
||||
};
|
||||
|
||||
const getPackageActionDetail = (job: JobApplication) => {
|
||||
const missingTailoredCv = !job.tailoredCvText;
|
||||
const missingNotes = !job.notes?.trim();
|
||||
|
||||
if (missingTailoredCv && missingNotes) return t("jobTablePackageMissingCvAndNotes");
|
||||
if (missingTailoredCv) return t("jobTableCvMissing");
|
||||
if (missingNotes) return t("jobTablePackageMissingNotes");
|
||||
return null;
|
||||
};
|
||||
|
||||
const getActionSignals = (job: JobApplication) => {
|
||||
const signals: Array<{
|
||||
label: string;
|
||||
detail: string;
|
||||
onClick: () => void;
|
||||
variant: "contained" | "outlined";
|
||||
color?: "warning" | "primary";
|
||||
}> = [];
|
||||
|
||||
if (job.needsFollowUp) {
|
||||
signals.push({
|
||||
label: t("jobTableFollowUp"),
|
||||
detail: job.followUpReason ?? t("jobTableNeedsFollowUp"),
|
||||
onClick: () => openFollowUpWorkspace(job.id),
|
||||
variant: "contained",
|
||||
color: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
const packageDetail = !job.isDeleted ? getPackageActionDetail(job) : null;
|
||||
if (packageDetail) {
|
||||
signals.push({
|
||||
label: t("jobTablePackageWork"),
|
||||
detail: packageDetail,
|
||||
onClick: () => openTailoredCvWorkspace(job.id),
|
||||
variant: job.needsFollowUp ? "outlined" : "contained",
|
||||
color: job.needsFollowUp ? "primary" : "warning",
|
||||
});
|
||||
}
|
||||
|
||||
return signals;
|
||||
};
|
||||
|
||||
const getPrimaryAction = (job: JobApplication) => getActionSignals(job)[0] ?? null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
||||
@@ -368,6 +421,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const open = expanded.includes(job.id);
|
||||
const toneName = statusTone(job.status);
|
||||
const tone = toneName === "error" ? theme.palette.error.main : toneName === "warning" ? theme.palette.warning.main : toneName === "success" ? theme.palette.success.main : toneName === "info" ? theme.palette.info.main : theme.palette.primary.main;
|
||||
const primaryAction = getPrimaryAction(job);
|
||||
const actionSignals = getActionSignals(job);
|
||||
return (
|
||||
<React.Fragment key={job.id}>
|
||||
<TableRow sx={{ backgroundColor: alpha(tone, theme.palette.mode === "dark" ? 0.1 : 0.06) }}>
|
||||
@@ -377,9 +432,20 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<span>{job.jobTitle}</span>
|
||||
{job.needsFollowUp ? <Chip size="small" label={t("jobTableFollowUp")} title={job.followUpReason ?? undefined} sx={{ fontWeight: 800, cursor: "pointer" }} onClick={() => navigate(buildJobWorkspacePath(job.id, { tab: JOB_DETAILS_TABS.followUp, followMode: "waiting-update" }))} /> : null}
|
||||
{!job.tailoredCvText && !job.isDeleted ? <Chip size="small" label={t("jobTableCvMissing")} color="warning" variant="outlined" sx={{ cursor: "pointer" }} onClick={() => navigate(buildJobWorkspacePath(job.id, { tab: JOB_DETAILS_TABS.tailoredCv }))} /> : null}
|
||||
{job.tailoredCvText ? <Chip size="small" label={t("jobTableCvReady")} color="success" variant="outlined" sx={{ cursor: "pointer" }} onClick={() => navigate(buildJobWorkspacePath(job.id, { tab: JOB_DETAILS_TABS.tailoredCv }))} /> : null}
|
||||
{actionSignals.map((signal) => (
|
||||
<Chip
|
||||
key={`${job.id}-${signal.label}-${signal.detail}`}
|
||||
size="small"
|
||||
label={signal.label}
|
||||
color={signal.color}
|
||||
variant={signal.variant === "contained" ? "filled" : "outlined"}
|
||||
title={signal.detail}
|
||||
sx={{ fontWeight: 800, cursor: "pointer" }}
|
||||
clickable
|
||||
onClick={signal.onClick}
|
||||
aria-label={`${job.jobTitle} — ${signal.label} signal`}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</TableCell>
|
||||
{columns.status ? <TableCell><Chip label={normalizeStatus(job.status)} size="small" color={toneName as any} /></TableCell> : null}
|
||||
@@ -387,11 +453,26 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
{columns.daysSince ? <TableCell>{job.daysSince}</TableCell> : null}
|
||||
{columns.jobUrl ? <TableCell>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{t("jobTableLink")}</a> : ""}</TableCell> : null}
|
||||
<TableCell align="right">
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 0.5 }}>
|
||||
<Tooltip title={t("jobTableEdit")}><IconButton size="small" onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton size="small" onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableOpen")}><IconButton size="small" onClick={() => setDetailsJobId(job.id)}><LaunchIcon fontSize="small" /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title={t("jobTableRestore")}><IconButton size="small" onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title={t("jobTableSoftDelete")}><IconButton size="small" onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
||||
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 0.75 }}>
|
||||
{primaryAction ? (
|
||||
<>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", fontWeight: 700 }}>
|
||||
{t("editJobNextAction")}
|
||||
</Typography>
|
||||
<Button size="small" variant={primaryAction.variant} color={primaryAction.color} onClick={primaryAction.onClick} aria-label={`${t("editJobNextAction")}: ${job.jobTitle} — ${primaryAction.label}`}>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", maxWidth: 220, textAlign: "right" }}>
|
||||
{primaryAction.detail}
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 0.5 }}>
|
||||
<Tooltip title={t("jobTableEdit")}><IconButton size="small" onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton size="small" onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableOpen")}><IconButton size="small" onClick={() => setDetailsJobId(job.id)}><LaunchIcon fontSize="small" /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title={t("jobTableRestore")}><IconButton size="small" onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title={t("jobTableSoftDelete")}><IconButton size="small" onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
Reference in New Issue
Block a user