feat(S05/T01): Unified workflow trust signals across the API, table, da…
- JobTrackerApi/Controllers/JobApplicationsController.cs - JobTrackerApi.Tests/JobApplicationsWorkflowSignalsTests.cs - job-tracker-ui/src/jobWorkflowSignals.ts - job-tracker-ui/src/components/JobTable.tsx - job-tracker-ui/src/components/DashboardView.tsx - job-tracker-ui/src/components/RemindersView.tsx - job-tracker-ui/src/workflow-trust-signals.test.tsx
This commit is contained in:
@@ -47,29 +47,8 @@ import { useToast } from "../toast";
|
||||
import SavedViewsMenu, { SavedViewParams } from "./SavedViewsMenu";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { buildJobWorkspacePath, JOB_DETAILS_TABS } from "../jobWorkspaceRoute";
|
||||
|
||||
interface JobApplication {
|
||||
id: number;
|
||||
jobTitle: string;
|
||||
status: string;
|
||||
dateApplied: string;
|
||||
daysSince: number;
|
||||
jobUrl?: string | null;
|
||||
notes?: string | null;
|
||||
location?: string | null;
|
||||
salary?: string | null;
|
||||
tags?: string | null;
|
||||
description?: string | null;
|
||||
isDeleted?: boolean;
|
||||
company: { name: string };
|
||||
companyId?: number;
|
||||
needsFollowUp?: boolean;
|
||||
followUpReason?: string | null;
|
||||
shortSummary?: string | null;
|
||||
fullSummary?: string | null;
|
||||
tailoredCvText?: string | null;
|
||||
}
|
||||
import { JobApplication } from "../types";
|
||||
import { getWorkflowAction, needsInterviewPrep, needsWorkflowWork } from "../jobWorkflowSignals";
|
||||
|
||||
interface PagedResult<T> {
|
||||
items: T[];
|
||||
@@ -210,8 +189,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
if (readinessFilter === "all") return jobs;
|
||||
if (readinessFilter === "interview") return jobs.filter((job) => job.status === "Interview" || job.status === "Interviewing");
|
||||
return jobs.filter((job) => !job.tailoredCvText || !job.notes);
|
||||
if (readinessFilter === "interview") return jobs.filter((job) => needsInterviewPrep(job));
|
||||
return jobs.filter((job) => needsWorkflowWork(job));
|
||||
}, [jobs, readinessFilter]);
|
||||
|
||||
const selectedAllOnPage = filteredJobs.length > 0 && filteredJobs.every((job) => selectedIds.includes(job.id));
|
||||
@@ -289,58 +268,38 @@ 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 buildWorkflowActionDetail = (job: JobApplication) => getWorkflowAction(job, {
|
||||
packageWork: t("jobTablePackageWork"),
|
||||
followUp: t("jobTableFollowUp"),
|
||||
interviewPrep: t("jobTableInterviewStage"),
|
||||
readiness: t("jobTableReadiness"),
|
||||
});
|
||||
|
||||
const getActionSignals = (job: JobApplication) => {
|
||||
const signals: Array<{
|
||||
label: string;
|
||||
detail: string;
|
||||
onClick: () => void;
|
||||
variant: "contained" | "outlined";
|
||||
color?: "warning" | "primary";
|
||||
}> = [];
|
||||
const action = buildWorkflowActionDetail(job);
|
||||
if (!action || job.isDeleted) return [];
|
||||
|
||||
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;
|
||||
return [{
|
||||
label: action.label,
|
||||
detail: action.detail,
|
||||
onClick: () => navigate(action.path),
|
||||
variant: action.key === "follow-up" ? "contained" as const : "outlined" as const,
|
||||
color: action.key === "follow-up" ? "warning" as const : "primary" as const,
|
||||
}];
|
||||
};
|
||||
|
||||
const getPrimaryAction = (job: JobApplication) => getActionSignals(job)[0] ?? null;
|
||||
const getPrimaryAction = (job: JobApplication) => {
|
||||
const action = buildWorkflowActionDetail(job);
|
||||
if (!action || job.isDeleted) return null;
|
||||
|
||||
return {
|
||||
label: action.label,
|
||||
detail: action.detail,
|
||||
onClick: () => navigate(action.path),
|
||||
variant: action.key === "follow-up" ? "contained" as const : "outlined" as const,
|
||||
color: action.key === "follow-up" ? "warning" as const : "primary" as const,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
Reference in New Issue
Block a user