using JobTrackerApi.Data; using JobTrackerApi.Models; namespace JobTrackerApi.Services; // Phase 5.6 — emitting the lifecycle events the timeline already knows how to read. // // JobEvent stays the single history source: this adds rows to it, it does not add a second store. // Every emitter lives here rather than at each call site, so the two status-change boundaries in // JobApplicationsController cannot drift apart, and a third boundary gets the behaviour for free. // // Events are derived from the TRANSITION, not from the resulting state, so one user action produces // at most one lifecycle event and re-saving an unchanged status produces none. // docs/architecture/application-workspace.md. public static class JobLifecycleEvents { // Records the status change itself plus, when the transition warrants it, one lifecycle event. // Replaces the hand-written StatusChanged block at each call site. public static void RecordStatusChange(JobTrackerContext db, JobApplication job, string? oldStatus, DateTime at) { var from = oldStatus ?? string.Empty; var to = job.Status ?? string.Empty; if (string.Equals(from, to, StringComparison.OrdinalIgnoreCase)) return; db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "StatusChanged", OldValue = oldStatus, NewValue = job.Status, At = at, }); var lifecycle = LifecycleFor(from, to); if (lifecycle is null) return; db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = lifecycle, // Useful metadata: which transition produced it, so the timeline can say more than the type. OldValue = oldStatus, NewValue = job.Status, At = at, }); } // At most one lifecycle event per transition. Ordered so an Interview -> Offer move reports the // offer, which is the thing the user actually cares about. private static string? LifecycleFor(string from, string to) { var wasInterview = IsInterviewStage(from); var isInterview = IsInterviewStage(to); if (IsOfferStage(to) && !IsOfferStage(from)) return "OfferReceived"; if (isInterview && !wasInterview) return "InterviewScheduled"; // Only counts as completed when the application moved FORWARD out of the interview stage. // Dragging a card back to an earlier stage is a correction, not a completed interview. if (wasInterview && !isInterview && JobPipeline.OrderOf(to) > JobPipeline.OrderOf(from)) { return "InterviewCompleted"; } return null; } // Emitted when a follow-up checklist item is ticked off. The task itself stays a checklist item — // this only records that it happened. public static void RecordFollowUpCompleted(JobTrackerContext db, int jobApplicationId, string title) { db.JobEvents.Add(new JobEvent { JobApplicationId = jobApplicationId, Type = "FollowUpCompleted", NewValue = title, At = DateTime.Now, }); } private static bool IsInterviewStage(string? status) => (status ?? string.Empty).Contains("interview", StringComparison.OrdinalIgnoreCase); private static bool IsOfferStage(string? status) => (status ?? string.Empty).Contains("offer", StringComparison.OrdinalIgnoreCase); }