Files
jobtrackingapp/job-tracker-ui/src/types.ts
T
cesnimda 6db3bffb2f
CI and Deploy / test (pull_request) Successful in 2m8s
CI and Deploy / deploy (pull_request) Has been skipped
feat(email): add provider picker to Settings > Account
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>
2026-07-11 19:53:51 +02:00

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;
}