feat(workspace): Application Workspace foundation (Phase 5 milestone 1)
Every JobApplication gets a dedicated workspace at /applications/{id} — a
surface, not a new data store. It owns no data and duplicates none: CV comes
from the Phase 4 CvVariant lens, analysis/match/interview from the existing
AiWorkspacePanel, documents from Attachments, communication from
Correspondence, activity from JobEvent, stage semantics from JobPipeline. No
career data is copied and nothing here writes.
- GET /api/jobapplications/{id}/workspace: one aggregate read (role, company,
stage, dates, attached CV variant, cover letter, documents, AI history,
recent activity) replacing the page fanning out across endpoints
- Next recommended action: ordered rules answering "what do I do next?", the
core product principle for this phase
- ApplicationWorkspacePage: left nav + linkable ?section=, reusing the existing
component for each domain; later-milestone sections say so rather than faking
- Entry point from the job dialog via an optional onOpenWorkspace callback —
the dialog must not depend on router context (it is mounted without a
<Router> in several suites), so the caller owns navigation
- 8 backend tests (aggregate, CV variant surfacing, counts, activity ordering,
next-step rules, tenant scoping)
Local: 314 backend, 88/88 frontend (31 suites), tsc clean, production build ok.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { api } from "./api";
|
||||
|
||||
// Mirrors WorkspaceOverviewDto. Read-only aggregate — the workspace owns no data.
|
||||
export type WorkspaceCv = {
|
||||
variantId: number | null;
|
||||
variantName: string | null;
|
||||
themeId: string | null;
|
||||
hasTailoredCvText: boolean;
|
||||
updatedAtUtc: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceActivity = { type: string; detail: string | null; at: string };
|
||||
export type WorkspaceNextStep = { key: string; label: string; reason: string; section: string | null };
|
||||
|
||||
export type WorkspaceOverview = {
|
||||
id: number;
|
||||
jobTitle: string;
|
||||
company: string | null;
|
||||
location: string | null;
|
||||
salary: string | null;
|
||||
status: string;
|
||||
stageGroup: string;
|
||||
stageOrder: number;
|
||||
dateApplied: string | null;
|
||||
deadline: string | null;
|
||||
followUpAt: string | null;
|
||||
nextAction: string | null;
|
||||
jobUrl: string | null;
|
||||
hasJobDescription: boolean;
|
||||
cv: WorkspaceCv;
|
||||
hasCoverLetter: boolean;
|
||||
documentCount: number;
|
||||
hasPortfolio: boolean;
|
||||
aiInteractionCount: number;
|
||||
lastAiAtUtc: string | null;
|
||||
recentActivity: WorkspaceActivity[];
|
||||
nextStep: WorkspaceNextStep | null;
|
||||
};
|
||||
|
||||
// Workspace navigation. Sections map to the Phase 5 milestones; each is added as its milestone lands
|
||||
// so the workspace is always usable rather than a shell of placeholders.
|
||||
export type WorkspaceSectionKey =
|
||||
| "overview" | "job-details" | "analysis" | "match" | "checklist" | "cv" | "cover-letter"
|
||||
| "portfolio" | "documents" | "interview" | "timeline" | "notes" | "communication";
|
||||
|
||||
export const WORKSPACE_SECTIONS: { key: WorkspaceSectionKey; label: string; milestone?: number }[] = [
|
||||
{ key: "overview", label: "Overview" },
|
||||
{ key: "job-details", label: "Job Details" },
|
||||
{ key: "analysis", label: "Analysis" },
|
||||
{ key: "match", label: "Match" },
|
||||
{ key: "checklist", label: "Checklist", milestone: 2 },
|
||||
{ key: "cv", label: "CV", milestone: 6 },
|
||||
{ key: "cover-letter", label: "Cover Letter", milestone: 7 },
|
||||
{ key: "portfolio", label: "Portfolio", milestone: 8 },
|
||||
{ key: "documents", label: "Documents" },
|
||||
{ key: "interview", label: "Interview Prep" },
|
||||
{ key: "timeline", label: "Timeline", milestone: 3 },
|
||||
{ key: "notes", label: "Notes", milestone: 3 },
|
||||
{ key: "communication", label: "Communication" },
|
||||
];
|
||||
|
||||
export const applicationWorkspaceApi = {
|
||||
overview: (jobId: number) =>
|
||||
api.get<WorkspaceOverview>(`/jobapplications/${jobId}/workspace`).then((r) => r.data),
|
||||
};
|
||||
Reference in New Issue
Block a user