/ lib / types.ts
types.ts
  1  // ============================================================================
  2  // GapZero — Type Definitions
  3  // All interfaces match the PRD data schemas and SAMPLE_ANALYSIS.json structure
  4  // ============================================================================
  5  
  6  import type { GitHubAnalysis } from '@/lib/prompts/github-analysis';
  7  import type { CoverLetter } from '@/lib/prompts/cover-letter';
  8  
  9  // Re-export for consumers
 10  export type { GitHubAnalysis, CoverLetter };
 11  
 12  // --- Input Types ---
 13  
 14  export interface CareerQuestionnaire {
 15    currentRole: string;
 16    targetRole: string;
 17    targetRole2?: string;       // Optional alternative target role
 18    targetRole3?: string;       // Optional alternative target role
 19    yearsExperience: number;
 20    country: string;
 21    workPreference: 'remote' | 'hybrid' | 'onsite' | 'flexible';
 22    currentSalary?: number;
 23    targetSalary?: number;
 24    jobPosting?: string;        // Legacy single posting (backward compat)
 25    jobPostingUrl?: string;     // URL of a job posting (fetched server-side)
 26    jobPostings?: JobPostingInput[];  // Multiple postings for richer context
 27    language?: string;          // 'en' | 'ro' | 'de' — for localized AI responses
 28    linkedInProfile?: string;   // Raw LinkedIn profile text for supplementary data
 29    githubUrl?: string;         // GitHub profile URL for repo/language analysis
 30    additionalContext?: string; // User-provided context about career gaps, freelance work, etc.
 31  }
 32  
 33  export interface JobPostingInput {
 34    text: string;               // Full job posting text
 35    url?: string;               // Source URL if fetched
 36    title?: string;             // Extracted job title
 37  }
 38  
 39  // --- Output Types ---
 40  
 41  export interface AnalysisResult {
 42    metadata: AnalysisMetadata;
 43    fitScore: FitScore;
 44    strengths: Strength[];
 45    gaps: Gap[];
 46    roleRecommendations: RoleRecommendation[];
 47    actionPlan: ActionPlan;
 48    salaryAnalysis: SalaryAnalysis;
 49    jobMatch?: JobMatch;
 50    atsScore?: ATSScoreResult;
 51    profile?: ExtractedProfile;
 52    githubAnalysis?: GitHubAnalysis;
 53    coverLetter?: CoverLetter;
 54    interviewPrep?: InterviewPrep;
 55  }
 56  
 57  export interface AnalysisMetadata {
 58    analyzedAt: string;
 59    cvFileName: string;
 60    targetRole: string;
 61    country: string;
 62    /** Original job posting text (for CV re-scoring) */
 63    jobPosting?: string;
 64    /** GitHub profile URL for repo analysis */
 65    githubUrl?: string;
 66    /** Warning message if PDF text extraction quality was suboptimal */
 67    pdfQualityWarning?: string;
 68    /** Tracks which data came from Claude vs fallback */
 69    dataSources?: Record<string, 'claude' | 'fallback'>;
 70    /** Whether a real CV was uploaded (false = LinkedIn PDF only) */
 71    hasRealCV?: boolean;
 72    /** Pipeline coherence warnings (e.g., fitScore vs matchScore divergence) */
 73    warnings?: string[];
 74  }
 75  
 76  export interface FitScore {
 77    score: number;
 78    label: 'Strong Fit' | 'Moderate Fit' | 'Stretch' | 'Significant Gap';
 79    summary: string;
 80  }
 81  
 82  export interface Strength {
 83    title: string;
 84    description: string;
 85    relevance: string;
 86    tier: 'differentiator' | 'strong' | 'supporting';
 87  }
 88  
 89  export interface Gap {
 90    skill: string;
 91    severity: 'critical' | 'moderate' | 'minor';
 92    currentLevel: string;
 93    requiredLevel: string;
 94    impact: string;
 95    closingPlan: string;
 96    timeToClose: string;
 97    resources: string[];
 98  }
 99  
100  export interface RoleRecommendation {
101    title: string;
102    fitScore: number;
103    salaryRange: SalaryRange;
104    reasoning: string;
105    exampleCompanies: string[];
106    timeToReady: string;
107  }
108  
109  export interface SalaryRange {
110    low: number;
111    mid: number;
112    high: number;
113    currency: string;
114  }
115  
116  export interface ActionPlan {
117    thirtyDays: ActionItem[];
118    ninetyDays: ActionItem[];
119    twelveMonths: ActionItem[];
120  }
121  
122  export interface ActionItem {
123    action: string;
124    priority: 'critical' | 'high' | 'medium';
125    timeEstimate: string;
126    resource: string;
127    resourceUrl?: string;
128    expectedImpact: string;
129  }
130  
131  export interface SalaryAnalysis {
132    currentRoleMarket: MarketSalary;
133    targetRoleMarket: MarketSalary;
134    growthPotential: string;
135    bestMonetaryMove: string;
136    negotiationTips: string[];
137    dataSource?: SalaryDataSource;
138  }
139  
140  export interface MarketSalary {
141    low: number;
142    mid: number;
143    high: number;
144    currency: string;
145    region: string;
146    source?: SalaryDataSource;
147  }
148  
149  export interface MissingSkill {
150    skill: string;
151    /**
152     * "important": core requirement, must-have
153     * "not_a_deal_breaker": preferred, learnable on the job
154     * "quick_win": learnable in 1 week (tool/platform/syntax gap, not structural)
155     * "unimportant": tangential mention, easily substituted
156     */
157    importance: 'important' | 'not_a_deal_breaker' | 'quick_win' | 'unimportant';
158  }
159  
160  export interface JobMatch {
161    matchScore: number;
162    matchingSkills: string[];
163    missingSkills: MissingSkill[];
164    cvSuggestions: CVSuggestion[];
165    overallAdvice: string;
166    /** 1-2 bullets: what this candidate brings that most applicants won't */
167    competitiveAdvantage?: string[];
168  }
169  
170  export interface CVSuggestion {
171    section: string;
172    current: string;
173    suggested: string;
174    reasoning: string;
175  }
176  
177  // --- Interview Prep Types ---
178  
179  export interface InterviewPrep {
180    difficultyRating: 'easy' | 'moderate' | 'competitive' | 'highly_competitive';
181    difficultyRationale: string;
182    focusAreas: string[];
183    estimatedPrepHours: number;
184    technicalQuestions: TechnicalQuestion[];
185    behavioralQuestions: BehavioralQuestion[];
186    situationalQuestions: SituationalQuestion[];
187    cultureQuestions: CultureQuestion[];
188    technicalReview: TechnicalReviewItem[];
189    softSkills: SoftSkillPrep[];
190    questionsToAsk: QuestionToAsk[];
191    mentalReadinessTip: string;
192    testGorillaTests: string[];
193  }
194  
195  export interface TechnicalQuestion {
196    question: string;
197    difficulty: 'easy' | 'medium' | 'hard';
198    likelihood: 'very_likely' | 'possible';
199    testing: string;
200    approach: string;
201    yourEdge?: string;
202  }
203  
204  export interface BehavioralQuestion {
205    question: string;
206    competency: string;
207    starHints: {
208      situation: string;
209      task: string;
210      action: string;
211      result: string;
212    };
213  }
214  
215  export interface SituationalQuestion {
216    question: string;
217    framework: string;
218  }
219  
220  export interface CultureQuestion {
221    question: string;
222    suggestedAngle: string;
223  }
224  
225  export interface TechnicalReviewItem {
226    topic: string;
227    cluster: string;
228    whyItMatters: string;
229    whatToReview: string;
230    estimatedTime: '30min' | '1-2h' | 'half-day';
231    searchTerm: string;
232  }
233  
234  export interface SoftSkillPrep {
235    skill: string;
236    inContext: string;
237    howToSignal: string[];
238    redFlag: string;
239  }
240  
241  export interface QuestionToAsk {
242    question: string;
243    category: 'role' | 'team' | 'company' | 'growth' | 'strategic';
244  }
245  
246  // --- Intermediate Types (used between Claude calls) ---
247  
248  export interface ExtractedProfile {
249    name: string;
250    currentRole: string;
251    totalYearsExperience: number;
252    skills: SkillCategory[];
253    certifications: string[];
254    education: EducationItem[];
255    experience: ExperienceItem[];
256    languages: LanguageItem[];
257    summary: string;
258  }
259  
260  export interface SkillCategory {
261    category: string;
262    skills: string[];
263    proficiencyLevel: 'expert' | 'advanced' | 'intermediate' | 'beginner';
264  }
265  
266  export interface EducationItem {
267    degree: string;
268    institution: string;
269    year?: string;
270    field: string;
271  }
272  
273  export interface ExperienceItem {
274    title: string;
275    company: string;
276    duration: string;
277    highlights: string[];
278    technologies: string[];
279  }
280  
281  export interface LanguageItem {
282    language: string;
283    level: string;
284  }
285  
286  // --- Career Profile Types (persistent user data) ---
287  
288  export interface CareerProfile {
289    userId: string;
290    currentRole: string | null;
291    targetRole: string | null;
292    yearsExperience: number | null;
293    country: string | null;
294    workPreference: 'remote' | 'hybrid' | 'onsite' | 'flexible' | null;
295    githubUrl: string | null;
296    cvStoragePath: string | null;
297    linkedinStoragePath: string | null;
298    cvFilename: string | null;
299    linkedinFilename: string | null;
300    extractedProfile: ExtractedProfile | null;
301    additionalContext: string | null;
302    createdAt: string;
303    updatedAt: string;
304  }
305  
306  export interface CareerProfileInput {
307    currentRole?: string;
308    targetRole?: string;
309    yearsExperience?: number;
310    country?: string;
311    workPreference?: 'remote' | 'hybrid' | 'onsite' | 'flexible';
312    githubUrl?: string;
313    cvFilename?: string;
314    linkedinFilename?: string;
315    extractedProfile?: ExtractedProfile;
316    additionalContext?: string;
317  }
318  
319  // --- API Request/Response Types ---
320  
321  export interface AnalyzeRequest {
322    cv: File;
323    questionnaire: CareerQuestionnaire;
324  }
325  
326  export interface MatchJobRequest {
327    cvText: string;
328    skills: string[];
329    jobPosting: string;
330  }
331  
332  export interface RewriteCVRequest {
333    cvText: string;
334    targetRole: string;
335    gaps: Gap[];
336    jobPosting?: string;
337  }
338  
339  export interface RewriteCVResponse {
340    suggestions: CVSuggestion[];
341    rewrittenSummary: string;
342  }
343  
344  // --- Error Types ---
345  
346  export interface APIError {
347    error: string;
348    message: string;
349    status: number;
350  }
351  
352  // --- Salary Data Source Types ---
353  
354  export type SalaryDataSource = 'government_bls' | 'government_ons' | 'government_eurostat' | 'survey_stackoverflow' | 'market' | 'estimate';
355  
356  // --- Utility Types ---
357  
358  export type SeverityColor = {
359    critical: string;
360    moderate: string;
361    minor: string;
362  };
363  
364  export type TierColor = {
365    differentiator: string;
366    strong: string;
367    supporting: string;
368  };
369  
370  // --- CV Generator types ---
371  
372  // --- ATS Scoring Types ---
373  
374  export interface ATSScoreResult {
375    overallScore: number; // 0-100
376    keywordScore: number; // 0-100 (weighted: required keywords count more)
377    formatScore: number; // 0-100
378    keywords: ATSKeywordAnalysis;
379    formatIssues: ATSFormatIssue[];
380    recommendations: ATSRecommendation[];
381    companyATS?: CompanyATSInfo;
382  }
383  
384  export interface ATSKeywordAnalysis {
385    matched: ATSKeyword[];
386    semanticMatch: ATSKeyword[]; // e.g., "React" ≈ "React.js"
387    missing: ATSKeyword[];
388    total: {
389      required: number;
390      matched: number;
391      missing: number;
392    };
393  }
394  
395  export interface ATSKeyword {
396    keyword: string;
397    category: 'required' | 'preferred' | 'nice-to-have';
398    matchedAs?: string; // what it matched in CV (for semantic matches)
399    cvSection?: string; // where found / where to add
400    importance: 'high' | 'medium' | 'low';
401  }
402  
403  export interface ATSFormatIssue {
404    issue: string;
405    severity: 'critical' | 'warning' | 'info';
406    description: string;
407    fix: string;
408  }
409  
410  export interface ATSRecommendation {
411    action: string;
412    section: string; // Which CV section to modify
413    priority: 'critical' | 'high' | 'medium';
414    keywords: string[]; // Which keywords this addresses
415    example?: string; // Example of what to add/change
416  }
417  
418  export interface CompanyATSInfo {
419    company: string;
420    atsSystem: string; // "Workday" | "Greenhouse" | "Lever" | "Taleo" | "iCIMS" | "SmartRecruiters" | "Unknown"
421    tips: string[];
422  }
423  
424  // --- ATS Score API Request/Response ---
425  
426  export interface ATSScoreRequest {
427    cvText: string;
428    jobPosting: string;
429    extractedSkills?: string[];
430    companyName?: string;
431    jobUrl?: string;
432  }
433  
434  // --- Validation Types ---
435  
436  export interface ValidationIssue {
437    section: string;           // 'fitScore' | 'strengths' | 'gaps' | 'actionPlan' | 'salaryAnalysis' | 'roleRecommendations' | 'jobMatch'
438    severity: 'error' | 'warning' | 'info';
439    field: string;             // specific field path, e.g., 'fitScore.score', 'gaps[0].severity'
440    message: string;           // human-readable issue description
441    autoFixable: boolean;      // can the system fix this automatically?
442    autoFixAction?: string;    // description of what auto-fix would do
443  }
444  
445  export interface ValidationReport {
446    isValid: boolean;          // true if no errors (warnings OK)
447    issues: ValidationIssue[];
448    autoFixed: number;         // count of issues auto-fixed
449    sections: Record<string, { valid: boolean; issueCount: number }>;
450    autoFixDescriptions: string[];  // human-readable descriptions of each auto-fix applied
451  }
452  
453  // --- CV Generator types ---
454  
455  export interface GeneratedCV {
456    professionalSummary: string;
457    skills: {
458      category: string;
459      items: string[];
460    }[];
461    experienceBullets: {
462      role: string;
463      company: string;
464      bullets: string[];
465    }[];
466    certifications: string[];
467    projectHighlights: {
468      name: string;
469      description: string;
470      technologies: string[];
471    }[];
472    coverLetterDraft: string;
473  }
474  
475  // --- Job Tracker Types ---
476  
477  export type JobStatus = 'saved' | 'applied' | 'interviewing' | 'offer' | 'rejected' | 'withdrawn';
478  
479  export type JobSource = 'manual' | 'upwork' | 'linkedin';
480  
481  export interface JobApplication {
482    id: string;
483    userId: string;
484    company: string;
485    roleTitle: string;
486    jobUrl?: string;
487    jobPostingText?: string;
488    location?: string;
489    workType: 'remote' | 'hybrid' | 'onsite' | 'flexible';
490    salaryMin?: number;
491    salaryMax?: number;
492    currency: string;
493    status: JobStatus;
494    statusUpdatedAt: string;
495    appliedAt?: string;
496    followUpAt?: string;
497    analysisId?: string;
498    matchScore?: number;
499    notes?: string;
500    contactName?: string;
501    contactEmail?: string;
502    source: JobSource;
503    metadata?: Record<string, unknown>;
504    sortOrder: number;
505    createdAt: string;
506    updatedAt: string;
507  }
508  
509  export interface JobApplicationInput {
510    company: string;
511    roleTitle: string;
512    jobUrl?: string;
513    jobPostingText?: string;
514    location?: string;
515    workType?: 'remote' | 'hybrid' | 'onsite' | 'flexible';
516    salaryMin?: number;
517    salaryMax?: number;
518    currency?: string;
519    status?: JobStatus;
520    appliedAt?: string;
521    followUpAt?: string;
522    notes?: string;
523    contactName?: string;
524    contactEmail?: string;
525    source?: JobSource;
526    metadata?: Record<string, unknown>;
527  }
528  
529  export interface JobTrackerStats {
530    total: number;
531    byStatus: Record<JobStatus, number>;
532    avgMatchScore: number | null;
533    followUpsDue: number;
534    appliedThisWeek: number;
535    appliedThisMonth: number;
536  }
537  
538  export interface KanbanColumn {
539    id: JobStatus;
540    label: string;
541    color: string;
542    icon: string;
543  }
544  
545  // --- Quota & Subscription Types ---
546  
547  export type QuotaType = 'analysis' | 'cv_generation' | 'cover_letter' | 'coach_request';
548  
549  export type PlanType = 'free' | 'pro';
550  
551  export type SubscriptionStatus = 'active' | 'past_due' | 'canceled' | 'trialing';
552  
553  export interface QuotaCheck {
554    allowed: boolean;
555    used: number;
556    limit: number;
557    plan: PlanType;
558    isInitialAnalysis?: boolean;
559    resetAt: string; // next Monday 00:00 UTC ISO string
560  }
561  
562  export interface UserQuotaStatus {
563    plan: PlanType;
564    weekStart: string;
565    resetAt: string;
566    analysis: { used: number; limit: number };
567    cvGeneration: { used: number; limit: number };
568    coverLetter: { used: number; limit: number };
569    coachRequest: { used: number; limit: number };
570    hasUsedInitialAnalysis: boolean;
571    subscription: {
572      status: SubscriptionStatus | null;
573      periodEnd: string | null;
574    } | null;
575  }
576  
577  export interface UserQuotaRow {
578    user_id: string;
579    plan: PlanType;
580    week_start: string;
581    analyses_used: number;
582    cv_generations_used: number;
583    cover_letters_used: number;
584    coach_requests_used: number;
585    analyses_limit: number;
586    cv_limit: number;
587    cover_letter_limit: number;
588    coach_limit: number;
589    has_used_initial_analysis: boolean;
590    stripe_customer_id: string | null;
591    stripe_subscription_id: string | null;
592    subscription_status: SubscriptionStatus | null;
593    subscription_period_end: string | null;
594  }
595  
596  // --- Output Tag Types ---
597  
598  export type OutputTagType = 'accurate' | 'inaccurate' | 'irrelevant' | 'missing_context' | 'too_generic';
599  
600  export interface OutputTag {
601    id: string;
602    userId: string;
603    analysisId: string;
604    section: string;
605    elementIndex: number | null;
606    elementKey: string | null;
607    taggedText: string | null;
608    tag: OutputTagType;
609    comment: string | null;
610    createdAt: string;
611  }
612  
613  export interface OutputTagInput {
614    analysisId: string;
615    section: string;
616    elementIndex?: number;
617    elementKey?: string;
618    taggedText?: string;
619    tag: OutputTagType;
620    comment?: string;
621  }
622  
623  // --- Upwork Types ---
624  
625  export interface UpworkProfile {
626    name: string;
627    title: string;
628    overview: string;
629    hourlyRate?: number;
630    currency?: string;
631    totalEarnings?: number;
632    totalJobs?: number;
633    totalHours?: number;
634    jobSuccessScore?: number;
635    profileUrl?: string;
636    location?: string;
637    memberSince?: string;
638    skills: string[];
639    categories: string[];
640    employmentHistory: UpworkEmployment[];
641    education: UpworkEducation[];
642    certifications: string[];
643    portfolio: UpworkPortfolioItem[];
644    workHistory: UpworkJobHistory[];
645    languages: Array<{ language: string; proficiency: string }>;
646    availability?: string;
647    responseTime?: string;
648  }
649  
650  export interface UpworkEmployment {
651    title: string;
652    company: string;
653    startDate: string;
654    endDate?: string;
655    description: string;
656  }
657  
658  export interface UpworkEducation {
659    degree: string;
660    institution: string;
661    year?: string;
662  }
663  
664  export interface UpworkPortfolioItem {
665    title: string;
666    description: string;
667    url?: string;
668  }
669  
670  export interface UpworkJobHistory {
671    title: string;
672    client?: string;
673    dateRange: string;
674    earnings?: number;
675    hours?: number;
676    feedback?: string;
677    rating?: number;
678    skills: string[];
679    description: string;
680  }
681  
682  export interface UpworkJobPosting {
683    title: string;
684    description: string;
685    clientInfo: {
686      country?: string;
687      paymentVerified?: boolean;
688      totalSpent?: string;
689      hireRate?: string;
690      totalJobs?: number;
691      avgHourlyRate?: string;
692      companySize?: string;
693      memberSince?: string;
694      rating?: number;
695    };
696    budget: {
697      type: 'hourly' | 'fixed';
698      min?: number;
699      max?: number;
700      currency?: string;
701    };
702    skills: string[];
703    experienceLevel?: 'entry' | 'intermediate' | 'expert';
704    projectLength?: string;
705    weeklyHours?: string;
706    screeningQuestions: UpworkScreeningQuestion[];
707    proposals?: number;
708    connects?: number;
709    postedDate?: string;
710    category?: string;
711    subcategory?: string;
712  }
713  
714  export interface UpworkScreeningQuestion {
715    question: string;
716    type: 'text' | 'yesno' | 'choice' | 'attachment';
717    required: boolean;
718    order: number;
719    options?: string[];
720    maxLength?: number;
721  }
722  
723  export interface UpworkCoverLetter {
724    openingHook: string;
725    screeningAnswers: UpworkScreeningAnswer[];
726    body: string;
727    closingCta: string;
728    suggestedRate: {
729      amount: number;
730      type: 'hourly' | 'fixed';
731      currency: string;
732      reasoning: string;
733    };
734    profileOptimization: string[];
735  }
736  
737  export interface UpworkScreeningAnswer {
738    question: string;
739    answer: string;
740    order: number;
741    strategy: string;
742  }
743  
744  export interface UpworkProfileAnalysis {
745    overallScore: number;
746    profileStrengths: Array<{
747      area: string;
748      description: string;
749      impact: string;
750    }>;
751    profileWeaknesses: Array<{
752      area: string;
753      description: string;
754      fix: string;
755      priority: 'critical' | 'high' | 'medium';
756    }>;
757    titleOptimization: {
758      current: string;
759      suggested: string;
760      reasoning: string;
761    };
762    overviewRewrite: {
763      current: string;
764      suggested: string;
765      reasoning: string;
766    };
767    rateAdvice: {
768      currentRate?: number;
769      suggestedRange: { min: number; max: number };
770      reasoning: string;
771      positioningStrategy: string;
772    };
773    skillsAdvice: {
774      keep: string[];
775      add: string[];
776      remove: string[];
777      reorder: string[];
778    };
779    nichingStrategy: string;
780    proposalTips: string[];
781    competitivePosition: string;
782  }