109745edb0
Make /jobs/:id the canonical application workspace while preserving list state and compatibility links. Replace popup and expandable-row navigation with accessible whole-row routing and richer job details.
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
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;
|
|
savedAt: string;
|
|
description: string | null;
|
|
translatedDescription: string | null;
|
|
descriptionLanguage: string | null;
|
|
tags: string[];
|
|
notes: string | null;
|
|
source: string | null;
|
|
countryCode: string | null;
|
|
hasJobDescription: boolean;
|
|
cv: WorkspaceCv;
|
|
hasCoverLetter: boolean;
|
|
documentCount: number;
|
|
hasPortfolio: boolean;
|
|
aiInteractionCount: number;
|
|
lastAiAtUtc: string | null;
|
|
recentActivity: WorkspaceActivity[];
|
|
nextStep: WorkspaceNextStep | null;
|
|
checklistProgress: ChecklistProgress | null;
|
|
};
|
|
|
|
// Milestone 2 — the application checklist. One workflow surface: system items seed from the same
|
|
// readiness signals the backend already computed, and the user owns everything after that.
|
|
export type ChecklistStatus = "pending" | "done" | "dismissed";
|
|
|
|
export type ChecklistItem = {
|
|
id: number;
|
|
systemKey: string | null;
|
|
title: string;
|
|
description: string | null;
|
|
category: string;
|
|
status: ChecklistStatus;
|
|
section: string | null;
|
|
sortOrder: number;
|
|
isSystemGenerated: boolean;
|
|
isAutoCompleted: boolean;
|
|
completedAt: string | null;
|
|
};
|
|
|
|
export type ChecklistProgress = { total: number; completed: number; dismissed: number; percent: number };
|
|
export type Checklist = { items: ChecklistItem[]; progress: ChecklistProgress };
|
|
|
|
export const CHECKLIST_CATEGORIES: { key: string; label: string }[] = [
|
|
{ key: "preparation", label: "Before applying" },
|
|
{ key: "submission", label: "Submitting" },
|
|
{ key: "follow-up", label: "Follow-up" },
|
|
{ key: "interview", label: "Interview" },
|
|
{ key: "custom", label: "Your own tasks" },
|
|
];
|
|
|
|
// 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"
|
|
| "documents" | "interview" | "timeline" | "communication";
|
|
|
|
export const WORKSPACE_SECTIONS: { key: WorkspaceSectionKey; label: string }[] = [
|
|
{ key: "overview", label: "Overview" },
|
|
{ key: "job-details", label: "Job Details" },
|
|
{ key: "analysis", label: "Analysis" },
|
|
{ key: "match", label: "Match" },
|
|
{ key: "checklist", label: "Checklist" },
|
|
{ key: "cv", label: "CV" },
|
|
{ key: "cover-letter", label: "Cover Letter" },
|
|
{ key: "documents", label: "Documents" },
|
|
{ key: "interview", label: "Interview Prep" },
|
|
{ key: "timeline", label: "Timeline" },
|
|
{ key: "communication", label: "Communication" },
|
|
];
|
|
|
|
export const workspaceSection = (value: string | null): WorkspaceSectionKey =>
|
|
WORKSPACE_SECTIONS.some((section) => section.key === value) ? value as WorkspaceSectionKey : "overview";
|
|
|
|
export const applicationWorkspaceApi = {
|
|
overview: (jobId: number) =>
|
|
api.get<WorkspaceOverview>(`/jobapplications/${jobId}/workspace`).then((r) => r.data),
|
|
};
|
|
|
|
// Phase 5.3 — Application Intelligence. All three reads are deterministic and read-only; the AI
|
|
// narrative stays on the existing /ai routes, which require the user to ask for it.
|
|
export type TimelineEvent = {
|
|
id: number;
|
|
type: string;
|
|
category: string;
|
|
summary: string;
|
|
detail: string | null;
|
|
isMilestone: boolean;
|
|
at: string;
|
|
};
|
|
|
|
export type TimelineDay = { date: string; label: string; events: TimelineEvent[] };
|
|
|
|
export type Timeline = {
|
|
days: TimelineDay[];
|
|
milestones: TimelineEvent[];
|
|
categories: string[];
|
|
totalEvents: number;
|
|
};
|
|
|
|
export type JobAnalysis = {
|
|
role: string | null;
|
|
company: string | null;
|
|
location: string | null;
|
|
employmentType: string | null;
|
|
seniority: string | null;
|
|
salary: string | null;
|
|
technologies: string[];
|
|
skills: string[];
|
|
responsibilities: string[];
|
|
keywords: string[];
|
|
summary: string;
|
|
importantRequirements: string[];
|
|
interviewTopics: string[];
|
|
missingInformation: string[];
|
|
hasJobDescription: boolean;
|
|
aiSuggestionCount: number;
|
|
};
|
|
|
|
export type MatchEvidence = { title: string; subtitle: string | null; matched: string[] };
|
|
|
|
export type CareerMatch = {
|
|
score: number;
|
|
band: string;
|
|
hasEnoughSignal: boolean;
|
|
hasCareerProfile: boolean;
|
|
matchedSkills: string[];
|
|
missingSkills: string[];
|
|
relevantExperience: MatchEvidence[];
|
|
relevantProjects: MatchEvidence[];
|
|
suggestions: string[];
|
|
aiSuggestionCount: number;
|
|
};
|
|
|
|
export const TIMELINE_CATEGORY_LABELS: Record<string, string> = {
|
|
lifecycle: "Lifecycle",
|
|
stage: "Stage",
|
|
"follow-up": "Follow-up",
|
|
communication: "Communication",
|
|
ai: "AI",
|
|
};
|
|
|
|
export const applicationIntelligenceApi = {
|
|
timeline: (jobId: number, category?: string, milestonesOnly?: boolean) =>
|
|
api
|
|
.get<Timeline>(`/jobapplications/${jobId}/timeline`, {
|
|
params: { category: category || undefined, milestonesOnly: milestonesOnly || undefined },
|
|
})
|
|
.then((r) => r.data),
|
|
analysis: (jobId: number) =>
|
|
api.get<JobAnalysis>(`/jobapplications/${jobId}/analysis`).then((r) => r.data),
|
|
match: (jobId: number) => api.get<CareerMatch>(`/jobapplications/${jobId}/match`).then((r) => r.data),
|
|
};
|
|
|
|
// Phase 5.4 — Application Assets. CV variant CRUD, preview, PDF export and version history stay on
|
|
// /api/cv (the existing CV builder). These types cover only what is application-scoped.
|
|
export type CvVariantSummary = {
|
|
id: number;
|
|
name: string;
|
|
themeId: string;
|
|
publicSlug: string;
|
|
isPublic: boolean;
|
|
version: number;
|
|
jobApplicationId: number | null;
|
|
updatedAtUtc: string;
|
|
};
|
|
|
|
export type ApplicationCv = {
|
|
attachedVariantId: number | null;
|
|
attachedVariantName: string | null;
|
|
attachedThemeId: string | null;
|
|
attachedVersion: number | null;
|
|
attachedUpdatedAtUtc: string | null;
|
|
attachedIsPublic: boolean;
|
|
hasTailoredCvText: boolean;
|
|
availableVariants: CvVariantSummary[];
|
|
};
|
|
|
|
export type TailoringSuggestion = { kind: string; title: string; detail: string | null; items: string[] };
|
|
|
|
export type TailoringPlan = {
|
|
hasJobDescription: boolean;
|
|
hasCareerProfile: boolean;
|
|
hasAttachedVariant: boolean;
|
|
matchScore: number;
|
|
suggestions: TailoringSuggestion[];
|
|
aiSuggestionCount: number;
|
|
};
|
|
|
|
export type CoverLetterVersion = {
|
|
version: number;
|
|
source: string;
|
|
aiAction: string | null;
|
|
length: number;
|
|
createdAtUtc: string;
|
|
isCurrent: boolean;
|
|
};
|
|
|
|
export type CoverLetter = {
|
|
text: string | null;
|
|
currentVersion: number;
|
|
versions: CoverLetterVersion[];
|
|
aiSuggestionCount: number;
|
|
};
|
|
|
|
export const applicationAssetsApi = {
|
|
cv: (jobId: number) => api.get<ApplicationCv>(`/jobapplications/${jobId}/cv`).then((r) => r.data),
|
|
attachVariant: (jobId: number, variantId: number | null) =>
|
|
api.put<ApplicationCv>(`/jobapplications/${jobId}/cv`, { variantId }).then((r) => r.data),
|
|
tailoring: (jobId: number) =>
|
|
api.get<TailoringPlan>(`/jobapplications/${jobId}/tailoring`).then((r) => r.data),
|
|
coverLetter: (jobId: number) =>
|
|
api.get<CoverLetter>(`/jobapplications/${jobId}/cover-letter`).then((r) => r.data),
|
|
saveCoverLetter: (jobId: number, text: string, source = "manual", aiAction?: string) =>
|
|
api.put<CoverLetter>(`/jobapplications/${jobId}/cover-letter`, { text, source, aiAction }).then((r) => r.data),
|
|
restoreCoverLetter: (jobId: number, version: number) =>
|
|
api.post<CoverLetter>(`/jobapplications/${jobId}/cover-letter/versions/${version}/restore`).then((r) => r.data),
|
|
};
|
|
|
|
// Phase 5.5 — Interview preparation and follow-up. Prep content is the user's; AI suggestions come
|
|
// from the existing /ai routes and only land here once accepted.
|
|
export type InterviewPrepItem = {
|
|
id: number;
|
|
category: string;
|
|
title: string;
|
|
content: string | null;
|
|
source: string;
|
|
isPrepared: boolean;
|
|
sortOrder: number;
|
|
updatedAtUtc: string;
|
|
};
|
|
|
|
export type InterviewPrepGroup = { category: string; label: string; items: InterviewPrepItem[] };
|
|
|
|
export type InterviewPrepBoard = {
|
|
groups: InterviewPrepGroup[];
|
|
total: number;
|
|
prepared: number;
|
|
percent: number;
|
|
isInterviewStage: boolean;
|
|
aiSuggestionCount: number;
|
|
};
|
|
|
|
export type FollowUp = {
|
|
followUpAt: string | null;
|
|
nextAction: string | null;
|
|
responseReceived: boolean;
|
|
openFollowUpTasks: number;
|
|
};
|
|
|
|
export const INTERVIEW_PREP_CATEGORIES: { key: string; label: string }[] = [
|
|
{ key: "company-research", label: "Company research" },
|
|
{ key: "technical", label: "Technical preparation" },
|
|
{ key: "behavioural", label: "Behavioural questions" },
|
|
{ key: "star", label: "STAR examples" },
|
|
{ key: "question", label: "Questions to ask them" },
|
|
{ key: "note", label: "Notes" },
|
|
];
|
|
|
|
export const interviewPrepApi = {
|
|
get: (jobId: number) =>
|
|
api.get<InterviewPrepBoard>(`/jobapplications/${jobId}/interview-prep`).then((r) => r.data),
|
|
add: (jobId: number, body: { category?: string; title: string; content?: string; source?: string }) =>
|
|
api.post<InterviewPrepItem>(`/jobapplications/${jobId}/interview-prep`, body).then((r) => r.data),
|
|
update: (jobId: number, itemId: number, body: Partial<Pick<InterviewPrepItem, "title" | "content" | "category" | "isPrepared">>) =>
|
|
api.patch<InterviewPrepItem>(`/jobapplications/${jobId}/interview-prep/${itemId}`, body).then((r) => r.data),
|
|
remove: (jobId: number, itemId: number) =>
|
|
api.delete(`/jobapplications/${jobId}/interview-prep/${itemId}`).then(() => undefined),
|
|
followUp: (jobId: number) =>
|
|
api.get<FollowUp>(`/jobapplications/${jobId}/follow-up`).then((r) => r.data),
|
|
setFollowUp: (jobId: number, followUpAt: string | null, nextAction?: string | null) =>
|
|
api.put<FollowUp>(`/jobapplications/${jobId}/follow-up`, { followUpAt, nextAction }).then((r) => r.data),
|
|
};
|
|
|
|
export const applicationChecklistApi = {
|
|
get: (jobId: number) =>
|
|
api.get<Checklist>(`/jobapplications/${jobId}/checklist`).then((r) => r.data),
|
|
add: (jobId: number, title: string, description?: string, category?: string) =>
|
|
api.post<ChecklistItem>(`/jobapplications/${jobId}/checklist`, { title, description, category }).then((r) => r.data),
|
|
update: (jobId: number, itemId: number, patch: Partial<Pick<ChecklistItem, "title" | "description" | "category" | "status">>) =>
|
|
api.patch<ChecklistItem>(`/jobapplications/${jobId}/checklist/${itemId}`, patch).then((r) => r.data),
|
|
remove: (jobId: number, itemId: number) =>
|
|
api.delete(`/jobapplications/${jobId}/checklist/${itemId}`).then(() => undefined),
|
|
reorder: (jobId: number, orderedIds: number[]) =>
|
|
api.put<Checklist>(`/jobapplications/${jobId}/checklist/order`, orderedIds).then((r) => r.data),
|
|
};
|