feat(workspace): interview and follow-up workflow
Phase 5.5. Completes the lifecycle after submission: prepare, communicate, chase. Interview preparation gets a durable, user-owned store. There were already two per-application AI stores, InterviewPrepNote and AiWorkspaceNote, but both are caches that regenerate when their context signature changes — anything a user typed into them would eventually be overwritten. InterviewPrepItem is the side nothing regenerates, covering company research, technical notes, behavioural answers, STAR examples and the user's own questions in one table, because those categories differ only by label and adding one must not need a migration. Each item records whether the user wrote it or accepted a suggestion, and an IsPrepared flag makes the section double as the preparation checklist. Generation stays in the existing AiWorkspaceService "interview" module, appended to AiInteraction as before. A suggestion is history until the user adds it as a prep item; opening the section generates nothing. Follow-up reuses what exists rather than adding a tracker. The date is JobApplication.FollowUpAt, the same field RulesEngine and the reminder hosted service already act on, so reminders keep working with no new wiring. The task stays an ApplicationChecklistItem in the follow-up category — the section counts open tasks without owning them. The record is a FollowUpSet JobEvent, the same type the rest of the app emits. Communication is untouched: Correspondence already owns recruiter contacts, history and notes, and the workspace already mounted it. The timeline interpreter learned five more types — InterviewScheduled, InterviewCompleted and OfferReceived as milestones, FollowUpCreated and FollowUpCompleted as routine, deliberately outside the milestone spine so it stays a summary of what actually happened. JobEvent remains the history source. InterviewPrepItems is reconciler-owned with a no-op migration, guarded on JobApplications, and verified on a fresh MariaDB 11: int AUTO_INCREMENT primary key, varchar owner and title, tinyint flag, datetime(6), composite index inside the key limit. 371 backend tests, 128 frontend tests, Release build and the production build all pass locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
namespace JobTrackerApi.Models;
|
||||
|
||||
// Phase 5.5 — Interview preparation the USER owns.
|
||||
//
|
||||
// Distinct from InterviewPrepNote and AiWorkspaceNote, which are AI caches: both are regenerated when
|
||||
// their context changes, so anything a user typed there would eventually be overwritten. This is the
|
||||
// durable side — company research, technical notes, behavioural answers, STAR examples and the user's
|
||||
// own questions — and nothing regenerates it.
|
||||
//
|
||||
// One table for every category rather than a table per category: they differ only by label, and a new
|
||||
// category must not need a migration. Reconciler-owned
|
||||
// (docs/infrastructure/database-ownership.md); its migration is a no-op.
|
||||
public sealed class InterviewPrepItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
public int JobApplicationId { get; set; }
|
||||
public JobApplication? JobApplication { get; set; }
|
||||
|
||||
// company-research | technical | behavioural | star | question | note
|
||||
public string Category { get; set; } = InterviewPrepCategories.Note;
|
||||
|
||||
// The prompt side: a question to answer, or the heading of a research note.
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
// The user's own words. Always theirs to edit — an AI suggestion only lands here once saved.
|
||||
public string? Content { get; set; }
|
||||
|
||||
// user | ai — whether the user wrote this or accepted it from a suggestion. Recorded for honesty
|
||||
// in the UI, not to restrict editing: an accepted suggestion is fully editable afterwards.
|
||||
public string Source { get; set; } = InterviewPrepSources.User;
|
||||
|
||||
// Practice tracking, so the section doubles as the preparation checklist.
|
||||
public bool IsPrepared { get; set; }
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public static class InterviewPrepCategories
|
||||
{
|
||||
public const string CompanyResearch = "company-research";
|
||||
public const string Technical = "technical";
|
||||
public const string Behavioural = "behavioural";
|
||||
public const string Star = "star";
|
||||
public const string Question = "question";
|
||||
public const string Note = "note";
|
||||
|
||||
// Display order in the workspace: understand the company, then the role, then yourself, then what
|
||||
// you want to ask them.
|
||||
public static readonly string[] Order =
|
||||
{
|
||||
CompanyResearch, Technical, Behavioural, Star, Question, Note,
|
||||
};
|
||||
|
||||
public static int Rank(string? category)
|
||||
{
|
||||
var i = Array.IndexOf(Order, category ?? Note);
|
||||
return i < 0 ? Order.Length : i;
|
||||
}
|
||||
|
||||
public static bool IsValid(string? value) => Array.IndexOf(Order, value ?? string.Empty) >= 0;
|
||||
}
|
||||
|
||||
public static class InterviewPrepSources
|
||||
{
|
||||
public const string User = "user";
|
||||
public const string Ai = "ai";
|
||||
|
||||
public static bool IsValid(string? value) => value is User or Ai;
|
||||
}
|
||||
Reference in New Issue
Block a user