6db3bffb2f
b5 of the multi-provider email roadmap (frontend). Adds EmailProviderConnections -- one card per provider (Gmail, Outlook/Microsoft 365, generic IMAP) showing connect status and connect/disconnect actions, mounted in SettingsView's Account tab alongside the existing app-login GoogleAuthCard (a separate concern: that card is sign-in identity, this is mailbox linking). Gmail and Microsoft reuse the OAuth-popup + postMessage handshake already built server-side (mirrors Correspondence.tsx's existing Gmail-connect flow). IMAP has no OAuth step, so it's a plain host/port/ssl/username/password form posting to /api/imap/connect, which verifies the connection server-side before storing it. Deliberately NOT touched: the Gmail-specific job-candidate-matching/review UI in Correspondence.tsx and GmailReviewPage.tsx. That backend pipeline (ListJobCandidateMessagesAsync, GmailReviewDecisions) is still Gmail-only by design -- generalising it now would mean building fake UI for capabilities Microsoft/IMAP don't have yet. This is scoped to the piece that's actually provider-neutral: connect/disconnect status. Verified live (backend + frontend dev servers): logged in, confirmed all three /status calls return 200, Gmail connect-url fetch succeeds, IMAP form submit hits /api/imap/connect and surfaces the expected 400 on a bad host. Frontend suite: 25 suites / 57 tests green (2 new). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
495 lines
11 KiB
TypeScript
495 lines
11 KiB
TypeScript
export interface Company {
|
|
id: number;
|
|
name: string;
|
|
location?: string;
|
|
source?: string;
|
|
recruiterName?: string;
|
|
recruiterEmail?: string;
|
|
recruiterLinkedIn?: string;
|
|
lastContactedAt?: string;
|
|
nextContactAt?: string;
|
|
pipelineStage?: string;
|
|
}
|
|
|
|
export type WorkflowActionKey = "package-work" | "follow-up" | "interview-prep" | "review-readiness";
|
|
export type WorkflowWorkspaceTab = "tailored-cv" | "follow-up" | "interview-prep" | "readiness";
|
|
|
|
export interface WorkflowSignal {
|
|
actionKey: WorkflowActionKey;
|
|
reason: string;
|
|
workspaceTab: WorkflowWorkspaceTab;
|
|
followMode?: string | null;
|
|
needsAttention: boolean;
|
|
hasPackageGap: boolean;
|
|
needsInterviewPrep: boolean;
|
|
needsFollowUpAction: boolean;
|
|
hasTailoredCv: boolean;
|
|
hasSavedApplicationAnswerDraft: boolean;
|
|
hasInterviewPrepNotes: boolean;
|
|
}
|
|
|
|
export interface TailoredCvExperienceItem {
|
|
title?: string | null;
|
|
company?: string | null;
|
|
location?: string | null;
|
|
start?: string | null;
|
|
end?: string | null;
|
|
isCurrent?: boolean;
|
|
bullets: string[];
|
|
}
|
|
|
|
export interface TailoredCvEducationItem {
|
|
qualification?: string | null;
|
|
qualificationLevel?: string | null;
|
|
institution?: string | null;
|
|
location?: string | null;
|
|
start?: string | null;
|
|
end?: string | null;
|
|
details: string[];
|
|
}
|
|
|
|
export interface TailoredCvCustomSection {
|
|
title?: string | null;
|
|
items: string[];
|
|
}
|
|
|
|
export interface TailoredCvRenderOptions {
|
|
showPhoto: boolean;
|
|
pageMode: string;
|
|
accentColor: string;
|
|
sectionOrder: string[];
|
|
bulletDensity: string;
|
|
}
|
|
|
|
export interface TailoredCvDraft {
|
|
id?: number | null;
|
|
canonicalProfileVersion?: number | null;
|
|
templateId: string;
|
|
headline?: string | null;
|
|
summary: string[];
|
|
selectedSkills: string[];
|
|
experience: TailoredCvExperienceItem[];
|
|
education: TailoredCvEducationItem[];
|
|
customSections: TailoredCvCustomSection[];
|
|
renderOptions: TailoredCvRenderOptions;
|
|
generationContextHash?: string | null;
|
|
lastGeneratedAtUtc?: string | null;
|
|
lastEditedAtUtc?: string | null;
|
|
status: string;
|
|
renderedText: string;
|
|
isLegacyFallback: boolean;
|
|
}
|
|
|
|
export interface JobApplication {
|
|
id: number;
|
|
jobTitle: string;
|
|
company: Company;
|
|
companyId?: number;
|
|
status: string;
|
|
dateApplied: string;
|
|
location?: string;
|
|
salary?: string;
|
|
salaryMin?: number | null;
|
|
salaryMax?: number | null;
|
|
salaryCurrency?: string | null;
|
|
salaryPeriod?: string | null;
|
|
nextAction?: string;
|
|
followUpAt?: string;
|
|
feedbackRequestedAt?: string;
|
|
responseReceived: boolean;
|
|
responseDate?: string;
|
|
description?: string;
|
|
translatedDescription?: string;
|
|
descriptionLanguage?: string;
|
|
tags?: string; // JSON array string
|
|
deadline?: string;
|
|
notes?: string;
|
|
coverLetterText?: string;
|
|
recruiterMessageDraft?: string | null;
|
|
jobUrl?: string;
|
|
shortSummary?: string;
|
|
fullSummary?: string | null;
|
|
tailoredCvText?: string | null;
|
|
tailoredCvUpdatedAt?: string | null;
|
|
workflowSignal?: WorkflowSignal | null;
|
|
|
|
hasResume?: boolean;
|
|
hasCoverLetter?: boolean;
|
|
hasPortfolio?: boolean;
|
|
hasOtherAttachment?: boolean;
|
|
|
|
daysSince: number;
|
|
isDeleted?: boolean;
|
|
deletedAt?: string;
|
|
needsFollowUp?: boolean;
|
|
followUpReason?: string | null;
|
|
}
|
|
|
|
export interface CandidateFitChannelGuidance {
|
|
cv: string[];
|
|
coverLetter: string[];
|
|
interview: string[];
|
|
recruiterMessage: string[];
|
|
}
|
|
|
|
export interface MatchScoreSectionCoverage {
|
|
section: string;
|
|
matched: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface StatusSuggestion {
|
|
hasSuggestion: boolean;
|
|
suggestedStatus?: string | null;
|
|
currentStatus?: string | null;
|
|
signal?: string | null;
|
|
confidence?: string | null;
|
|
messageDate?: string | null;
|
|
messageSubject?: string | null;
|
|
}
|
|
|
|
export interface MatchScore {
|
|
score: number;
|
|
band: string;
|
|
matchedCount: number;
|
|
totalKeywords: number;
|
|
matchedKeywords: string[];
|
|
missingKeywords: string[];
|
|
sectionCoverage: MatchScoreSectionCoverage[];
|
|
hasEnoughSignal: boolean;
|
|
}
|
|
|
|
export interface CandidateFit {
|
|
matchSummary: string;
|
|
fitLevel: string;
|
|
matchScore: number;
|
|
strengths: string[];
|
|
gaps: string[];
|
|
mention: string[];
|
|
avoid: string[];
|
|
cvImprovements: string[];
|
|
missingKeywords: string[];
|
|
interviewPrep: string[];
|
|
tailoredPitch: string;
|
|
guidance: CandidateFitChannelGuidance;
|
|
coverLetterDraft?: string | null;
|
|
recruiterMessageDraft?: string | null;
|
|
}
|
|
|
|
export interface FocusPlanResponse {
|
|
immediatePriorities: string[];
|
|
cvBulletIdeas: string[];
|
|
proofPointsToLeadWith: string[];
|
|
coverLetterAngles: string[];
|
|
followUpApproach: string[];
|
|
strategicSummary: string;
|
|
}
|
|
|
|
export interface InterviewPrepResponse {
|
|
summary: string;
|
|
talkingPoints: string[];
|
|
likelyQuestions: string[];
|
|
weakSpots: string[];
|
|
}
|
|
|
|
export interface ReadinessResponse {
|
|
score: number;
|
|
level: string;
|
|
completed: string[];
|
|
missing: string[];
|
|
reminders: string[];
|
|
workflowSignal: WorkflowSignal;
|
|
}
|
|
|
|
export interface FollowUpDraft {
|
|
subject: string;
|
|
body: string;
|
|
reason: string;
|
|
suggestedSendOn: string;
|
|
contextSummary: string;
|
|
contextSignals: string[];
|
|
threadSubject?: string | null;
|
|
lastCorrespondenceFrom?: string | null;
|
|
lastCorrespondenceAt?: string | null;
|
|
}
|
|
|
|
export interface ApplicationPackageResponse {
|
|
tailoredCvText: string;
|
|
coverLetterDraft?: string | null;
|
|
applicationAnswerDraft?: string | null;
|
|
recruiterMessageDraft?: string | null;
|
|
keyPoints: string[];
|
|
attachmentSignals: string[];
|
|
attachmentFilesUsed: string[];
|
|
coverLetterVariants: string[];
|
|
recruiterMessageVariants: string[];
|
|
}
|
|
|
|
export interface SaveApplicationDraftsRequest {
|
|
coverLetterText?: string | null;
|
|
notes?: string | null;
|
|
recruiterMessageDraft?: string | null;
|
|
}
|
|
|
|
export interface CorrespondenceAttachmentMetadata {
|
|
fileName?: string | null;
|
|
mimeType?: string | null;
|
|
sizeBytes?: number | null;
|
|
gmailAttachmentId?: string | null;
|
|
inline?: boolean;
|
|
}
|
|
|
|
export interface CorrespondenceMessage {
|
|
id: number;
|
|
jobApplicationId: number;
|
|
from: string;
|
|
direction?: string | null;
|
|
content: string;
|
|
subject?: string;
|
|
channel?: string;
|
|
date: string;
|
|
externalMessageId?: string | null;
|
|
externalThreadId?: string | null;
|
|
externalFrom?: string | null;
|
|
externalTo?: string | null;
|
|
externalLabelsJson?: string | null;
|
|
attachmentMetadataJson?: string | null;
|
|
}
|
|
|
|
export interface AttachmentItem {
|
|
id: number;
|
|
fileName: string;
|
|
uploadDate: string;
|
|
fileType: string;
|
|
fileSize: number;
|
|
purpose?: string | null;
|
|
useForAi: boolean;
|
|
}
|
|
|
|
export interface GmailJobMatchReason {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface GmailJobMatchedMessage {
|
|
id: string;
|
|
threadId: string;
|
|
subject: string;
|
|
from: string;
|
|
to: string;
|
|
date?: string;
|
|
snippet: string;
|
|
score: number;
|
|
confidence: string;
|
|
alreadyImported: boolean;
|
|
matchReasons: GmailJobMatchReason[];
|
|
}
|
|
|
|
export interface GmailJobMatchedThread {
|
|
threadId: string;
|
|
subject: string;
|
|
score: number;
|
|
confidence: string;
|
|
hasImportedMessages: boolean;
|
|
messageCount: number;
|
|
latestDate?: string;
|
|
matchReasons: GmailJobMatchReason[];
|
|
messages: GmailJobMatchedMessage[];
|
|
}
|
|
|
|
export interface GmailJobMatchesResponse {
|
|
jobApplicationId: number;
|
|
jobTitle: string;
|
|
companyName: string;
|
|
recruiterName?: string | null;
|
|
recruiterEmail?: string | null;
|
|
queries: string[];
|
|
threads: GmailJobMatchedThread[];
|
|
}
|
|
|
|
export interface GmailImportMessageResult {
|
|
imported: number;
|
|
skipped: number;
|
|
messageId: string;
|
|
threadId?: string | null;
|
|
message?: CorrespondenceMessage | null;
|
|
}
|
|
|
|
export interface GmailImportThreadResult {
|
|
imported: number;
|
|
skipped: number;
|
|
threadId?: string | null;
|
|
}
|
|
|
|
export interface GmailThreadRefreshThreadResult {
|
|
threadId: string;
|
|
imported: number;
|
|
skipped: number;
|
|
totalMessages: number;
|
|
status: string;
|
|
latestMessageDate?: string;
|
|
}
|
|
|
|
export interface GmailThreadRefreshResult {
|
|
jobApplicationId: number;
|
|
threadsChecked: number;
|
|
imported: number;
|
|
skipped: number;
|
|
hasLinkedThreads: boolean;
|
|
refreshedAt: string;
|
|
threads: GmailThreadRefreshThreadResult[];
|
|
}
|
|
|
|
export interface GmailReviewJobCandidate {
|
|
jobApplicationId: number;
|
|
jobTitle: string;
|
|
companyName: string;
|
|
score: number;
|
|
confidence: string;
|
|
reasons: GmailJobMatchReason[];
|
|
}
|
|
|
|
export interface GmailReviewThread {
|
|
threadId: string;
|
|
subject: string;
|
|
latestDate?: string;
|
|
messageCount: number;
|
|
routing: string;
|
|
hasImportedMessages: boolean;
|
|
decisionNote?: string | null;
|
|
matchedQueries: string[];
|
|
jobCandidates: GmailReviewJobCandidate[];
|
|
messages: GmailJobMatchedMessage[];
|
|
}
|
|
|
|
export interface GmailReviewQueueResponse {
|
|
queries: string[];
|
|
candidateThreadCount: number;
|
|
autoLinkThreadCount: number;
|
|
reviewThreadCount: number;
|
|
unmatchedThreadCount: number;
|
|
threads: GmailReviewThread[];
|
|
}
|
|
|
|
export interface GmailStatus {
|
|
connected: boolean;
|
|
gmailAddress?: string;
|
|
connectedAt?: string;
|
|
lastSyncedAt?: string;
|
|
lastSyncAttemptedAt?: string;
|
|
lastSyncSucceededAt?: string;
|
|
lastSyncMode?: string | null;
|
|
lastSyncSource?: string | null;
|
|
lastSyncStatus?: string | null;
|
|
lastSyncError?: string | null;
|
|
}
|
|
|
|
export interface MicrosoftGraphStatus {
|
|
connected: boolean;
|
|
mailAddress?: string | null;
|
|
connectedAt?: string;
|
|
lastSyncedAt?: string;
|
|
lastSyncAttemptedAt?: string;
|
|
lastSyncSucceededAt?: string;
|
|
lastSyncMode?: string | null;
|
|
lastSyncSource?: string | null;
|
|
lastSyncStatus?: string | null;
|
|
lastSyncError?: string | null;
|
|
}
|
|
|
|
export interface ImapStatus {
|
|
connected: boolean;
|
|
host?: string | null;
|
|
port?: number | null;
|
|
useSsl?: boolean | null;
|
|
username?: string | null;
|
|
connectedAt?: string;
|
|
lastSyncedAt?: string;
|
|
lastSyncAttemptedAt?: string;
|
|
lastSyncSucceededAt?: string;
|
|
lastSyncMode?: string | null;
|
|
lastSyncSource?: string | null;
|
|
lastSyncStatus?: string | null;
|
|
lastSyncError?: string | null;
|
|
}
|
|
|
|
export interface GmailManualSyncResult {
|
|
queriesRun: number;
|
|
candidateThreadCount: number;
|
|
autoLinkedThreadCount: number;
|
|
reviewThreadCount: number;
|
|
unmatchedThreadCount: number;
|
|
importedMessages: number;
|
|
importedThreads: number;
|
|
skippedMessages: number;
|
|
lookbackDays: number;
|
|
includeSpamTrash: boolean;
|
|
syncedAt: string;
|
|
}
|
|
|
|
export interface GmailSuggestedJobCandidate {
|
|
threadId: string;
|
|
subject: string;
|
|
latestDate?: string | null;
|
|
companyName?: string | null;
|
|
recruiterName?: string | null;
|
|
recruiterEmail?: string | null;
|
|
suggestedJobTitle?: string | null;
|
|
routing: string;
|
|
matchedQueries: string[];
|
|
preview: string;
|
|
}
|
|
|
|
export interface GmailSuggestedJobsResponse {
|
|
count: number;
|
|
items: GmailSuggestedJobCandidate[];
|
|
}
|
|
|
|
export interface CreatedSuggestedGmailJobResult {
|
|
jobApplicationId: number;
|
|
companyId: number;
|
|
threadId: string;
|
|
imported: number;
|
|
skipped: number;
|
|
}
|
|
|
|
export interface GmailRelinkResult {
|
|
threadId: string;
|
|
jobApplicationId: number;
|
|
imported: number;
|
|
skipped: number;
|
|
unlinkedMessages: number;
|
|
}
|
|
|
|
export interface GmailUnlinkResult {
|
|
threadId: string;
|
|
jobApplicationId: number;
|
|
removedMessages: number;
|
|
decision: string;
|
|
}
|
|
|
|
export interface GmailMessageSummary {
|
|
id: string;
|
|
threadId: string;
|
|
subject: string;
|
|
from: string;
|
|
to: string;
|
|
date?: string;
|
|
snippet: string;
|
|
}
|
|
|
|
export interface JobImportResult {
|
|
title?: string;
|
|
company?: string;
|
|
location?: string;
|
|
description?: string;
|
|
translatedDescription?: string;
|
|
language?: string;
|
|
tags: string[];
|
|
sourceUrl: string;
|
|
deadline?: string;
|
|
success: boolean;
|
|
parser?: string;
|
|
error?: string;
|
|
}
|