refactor(api): extract JobApplications DTOs and helpers, fix N+1 aggregation

- Move inline DTOs to JobApplicationDtos.cs, pure static helpers to JobApplicationHelpers.cs
- GetStats aggregates server-side (COUNT/GROUP BY) instead of loading the full table
- Cache RuleSettings via IMemoryCache, keyed per-user (RulesEngine.GetSettings falls back
  to per-user UserRuleSettings overrides, so a single global cache key would leak settings
  across users)
- Add missing AsNoTracking() to read-only GET endpoints (GetAll, GetById, GetBoard,
  GetReminders, GetStatusSuggestion, GetMatchScore, GetCandidateFit, GetFocusPlan,
  GetInterviewPrep, GetReadiness)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-12 20:08:59 +02:00
parent bd07876a41
commit ea6c3650f3
10 changed files with 936 additions and 875 deletions
@@ -0,0 +1,228 @@
using JobTrackerApi.Models;
namespace JobTrackerApi.Controllers
{
public sealed record TailoredCvPreviewDto(string TemplateId, string Html, string SuggestedFileName);
public sealed record TailoredCvRenderRequest(
string? TemplateId,
string? Headline,
List<string>? Summary,
List<string>? SelectedSkills,
List<TailoredCvExperienceItem>? Experience,
List<TailoredCvEducationItem>? Education,
List<TailoredCvCustomSection>? CustomSections,
TailoredCvRenderOptions? RenderOptions,
string? PhotoDataUrl,
bool? UseProfileAvatar);
public sealed record AttachmentContextResult(string Context, List<string> Signals, List<string> UsedFiles);
public sealed record CorrespondenceContextResult(string Context, List<string> Signals, List<string> Participants, List<string> ThreadIds);
public sealed record WorkflowSignalDto(
string ActionKey,
string Reason,
string WorkspaceTab,
string? FollowMode,
bool NeedsAttention,
bool HasPackageGap,
bool NeedsInterviewPrep,
bool NeedsFollowUpAction,
bool HasTailoredCv,
bool HasSavedApplicationAnswerDraft,
bool HasInterviewPrepNotes
);
public sealed record PagedResult<T>(List<T> Items, int Total, int Page, int PageSize);
public sealed record JobApplicationDto(
int Id,
int CompanyId,
Company Company,
string JobTitle,
string Status,
DateTime DateApplied,
bool ResponseReceived,
DateTime? ResponseDate,
string? Notes,
string? CoverLetterText,
string? JobUrl,
string? Description,
string? TranslatedDescription,
string? DescriptionLanguage,
string? Tags,
DateTime? Deadline,
string? Location,
string? Salary,
decimal? SalaryMin,
decimal? SalaryMax,
string? SalaryCurrency,
string? SalaryPeriod,
string? NextAction,
DateTime? FollowUpAt,
DateTime? FeedbackRequestedAt,
bool HasResume,
bool HasCoverLetter,
bool HasPortfolio,
bool HasOtherAttachment,
bool IsDeleted,
DateTime? DeletedAt,
int DaysSince,
bool NeedsFollowUp,
string? FollowUpReason,
string? TailoredCvText,
WorkflowSignalDto WorkflowSignal,
string? ShortSummary,
string? FullSummary
);
public sealed record CreateJobApplicationRequest(
string JobTitle,
int CompanyId,
string? Status,
string? Location,
string? Salary,
decimal? SalaryMin,
decimal? SalaryMax,
string? SalaryCurrency,
string? SalaryPeriod,
string? NextAction,
DateTime? FollowUpAt,
string? Notes,
string? Description,
string? TranslatedDescription,
string? DescriptionLanguage,
string? Tags,
DateTime? Deadline,
string? CoverLetterText,
string? JobUrl,
DateTime? DateApplied,
DateTime? FeedbackRequestedAt
);
public sealed record UpdateJobApplicationRequest(
string JobTitle,
int CompanyId,
string Status,
bool ResponseReceived,
DateTime? ResponseDate,
string? Location,
string? Salary,
decimal? SalaryMin,
decimal? SalaryMax,
string? SalaryCurrency,
string? SalaryPeriod,
string? NextAction,
DateTime? FollowUpAt,
string? Notes,
string? Description,
string? TranslatedDescription,
string? DescriptionLanguage,
string? Tags,
DateTime? Deadline,
string? CoverLetterText,
string? JobUrl,
DateTime? DateApplied,
DateTime? FeedbackRequestedAt,
DateTime? StatusChangedAt
);
public sealed record UpdateStatusRequest(string Status);
public sealed record PipelineStageDto(string Key, int Order, string Category);
public sealed record StatusSuggestionDto(
bool HasSuggestion,
string? SuggestedStatus,
string? CurrentStatus,
string? Signal,
string? Confidence,
DateTime? MessageDate,
string? MessageSubject);
public sealed record FollowUpRequest(DateTime? FollowUpAt);
public sealed record JobEventDto(int Id, string Type, string? OldValue, string? NewValue, string? Note, DateTime At);
public sealed record TimelineItemDto(string Kind, DateTime At, object Data);
public sealed record AnalyticsPoint(string Month, int Applied, int Responses);
public sealed record TagPoint(string Tag, int Count);
public sealed record TagTrendSeries(string Tag, List<int> Counts);
public sealed record TagTrendPoint(string Month, List<int> Counts);
public sealed record DuplicateCandidateDto(int Id, string JobTitle, string Company, string? JobUrl, string Status, DateTime DateApplied, string Reason);
public sealed record DuplicateCheckResult(bool HasDuplicates, List<DuplicateCandidateDto> Matches);
public sealed record FollowUpDraftDto(string Subject, string Body, string Reason, DateTime SuggestedSendOn, string ContextSummary, List<string> ContextSignals, string? ThreadSubject, string? LastCorrespondenceFrom, DateTime? LastCorrespondenceAt);
public sealed record FocusPlanDto(
List<string> ImmediatePriorities,
List<string> CvBulletIdeas,
List<string> ProofPointsToLeadWith,
List<string> CoverLetterAngles,
List<string> FollowUpApproach,
string StrategicSummary);
public sealed record SendFollowUpRequest(string? ToEmail, string Subject, string Body, DateTime? NextFollowUpAt);
public sealed record TagTrendResponse(List<string> Months, List<TagTrendSeries> Series);
public sealed record CandidateFitChannelGuidanceDto(List<string> Cv, List<string> CoverLetter, List<string> Interview, List<string> RecruiterMessage);
public sealed record CandidateFitDto(
string MatchSummary,
string FitLevel,
int MatchScore,
List<string> Strengths,
List<string> Gaps,
List<string> Mention,
List<string> Avoid,
List<string> CvImprovements,
List<string> MissingKeywords,
List<string> InterviewPrep,
string TailoredPitch,
CandidateFitChannelGuidanceDto Guidance,
string? CoverLetterDraft,
string? RecruiterMessageDraft);
public sealed record SaveTailoredCvRequest(string? TailoredCvText);
public sealed record TailoredCvDraftDto(
int? Id,
int? CanonicalProfileVersion,
string TemplateId,
string? Headline,
List<string> Summary,
List<string> SelectedSkills,
List<TailoredCvExperienceItem> Experience,
List<TailoredCvEducationItem> Education,
List<TailoredCvCustomSection> CustomSections,
TailoredCvRenderOptions RenderOptions,
string? GenerationContextHash,
DateTimeOffset? LastGeneratedAtUtc,
DateTimeOffset? LastEditedAtUtc,
string Status,
string RenderedText,
bool IsLegacyFallback);
public sealed record SaveTailoredCvDraftRequest(
string? TemplateId,
string? Headline,
List<string>? Summary,
List<string>? SelectedSkills,
List<TailoredCvExperienceItem>? Experience,
List<TailoredCvEducationItem>? Education,
List<TailoredCvCustomSection>? CustomSections,
TailoredCvRenderOptions? RenderOptions,
string? Status);
public sealed record GenerateApplicationPackageDto(string TailoredCvText, string? CoverLetterDraft, string? ApplicationAnswerDraft, string? RecruiterMessageDraft, List<string> KeyPoints, List<string> AttachmentSignals, List<string> AttachmentFilesUsed, List<string> CoverLetterVariants, List<string> RecruiterMessageVariants);
public sealed record SaveApplicationDraftsRequest(string? CoverLetterText, string? Notes, string? RecruiterMessageDraft);
public sealed record SavedPackageMaterial(string? TailoredCvText, string? CoverLetterText, string? RecruiterMessageDraft, string? Notes);
public sealed record InterviewPrepDto(string Summary, List<string> TalkingPoints, List<string> LikelyQuestions, List<string> WeakSpots);
public sealed record ReadinessDto(int Score, string Level, List<string> Completed, List<string> Missing, List<string> Reminders, WorkflowSignalDto WorkflowSignal);
public sealed record MatchScoreDto(
int Score,
string Band,
int MatchedCount,
int TotalKeywords,
List<string> MatchedKeywords,
List<string> MissingKeywords,
List<MatchSectionCoverageDto> SectionCoverage,
bool HasEnoughSignal);
public sealed record MatchSectionCoverageDto(string Section, int Matched, int Total);
}