peregrine/web/src/stores/interviews.ts
pyr0ball 0e4fce44c4 feat: shadow listing detector, hired feedback widget, contacts manager
Shadow listing detector (#95):
- Capture date_posted from JobSpy in discover.py + insert_job()
- Add date_posted migration to _MIGRATIONS
- _shadow_score() heuristic: 'shadow' (≥30 days stale), 'stale' (≥14 days)
- list_jobs() computes shadow_score per listing
- JobCard.vue: 'Ghost post' and 'Stale' badges with tooltip

Post-hire feedback widget (#91):
- Add hired_feedback migration to _MIGRATIONS
- POST /api/jobs/:id/hired-feedback endpoint
- InterviewCard.vue: optional widget on hired cards with factor
  checkboxes + freetext; dismissible; shows saved state
- PipelineJob interface extended with hired_feedback field

Contacts manager (#73):
- GET /api/contacts endpoint with job join, direction/search filters
- New ContactsView.vue: searchable table, inbound/outbound filter,
  signal chip column, job link
- Route /contacts added; Contacts nav link (UsersIcon) in AppNav

Also: add git to Dockerfile apt-get for circuitforge-core editable install
2026-04-15 08:34:12 -07:00

91 lines
3.6 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useApiFetch } from '../composables/useApi'
export interface StageSignal {
id: number // job_contacts.id — used for POST /api/stage-signals/{id}/dismiss
subject: string
received_at: string // ISO timestamp
stage_signal: 'interview_scheduled' | 'positive_response' | 'offer_received' | 'survey_received' | 'rejected'
body: string | null // email body text; null if not available
from_addr: string | null // sender address; null if not available
}
export interface PipelineJob {
id: number
title: string
company: string
url: string | null
location: string | null
is_remote: boolean
salary: string | null
match_score: number | null
keyword_gaps: string | null
status: string
interview_date: string | null
rejection_stage: string | null
applied_at: string | null
phone_screen_at: string | null
interviewing_at: string | null
offer_at: string | null
hired_at: string | null
survey_at: string | null
hired_feedback: string | null // JSON: { what_helped, factors }
stage_signals: StageSignal[] // undismissed signals, newest first
}
export const PIPELINE_STAGES = ['applied', 'survey', 'phone_screen', 'interviewing', 'offer', 'hired', 'interview_rejected'] as const
export type PipelineStage = typeof PIPELINE_STAGES[number]
export const STAGE_LABELS: Record<PipelineStage, string> = {
applied: 'Applied',
survey: 'Survey',
phone_screen: 'Phone Screen',
interviewing: 'Interviewing',
offer: 'Offer',
hired: 'Hired',
interview_rejected: 'Rejected',
}
export const useInterviewsStore = defineStore('interviews', () => {
const jobs = ref<PipelineJob[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const applied = computed(() => jobs.value.filter(j => j.status === 'applied'))
const survey = computed(() => jobs.value.filter(j => j.status === 'survey'))
const phoneScreen = computed(() => jobs.value.filter(j => j.status === 'phone_screen'))
const interviewing = computed(() => jobs.value.filter(j => j.status === 'interviewing'))
const offer = computed(() => jobs.value.filter(j => j.status === 'offer'))
const hired = computed(() => jobs.value.filter(j => j.status === 'hired'))
const offerHired = computed(() => jobs.value.filter(j => j.status === 'offer' || j.status === 'hired'))
const rejected = computed(() => jobs.value.filter(j => j.status === 'interview_rejected'))
async function fetchAll() {
loading.value = true
const { data, error: err } = await useApiFetch<PipelineJob[]>('/api/interviews')
loading.value = false
if (err) { error.value = 'Could not load interview pipeline'; return }
jobs.value = (data ?? []).map(j => ({ ...j }))
}
async function move(jobId: number, status: PipelineStage, opts: { interview_date?: string; rejection_stage?: string } = {}) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
const prevStatus = job.status
job.status = status
const { error: err } = await useApiFetch(`/api/jobs/${jobId}/move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, ...opts }),
})
if (err) {
job.status = prevStatus
error.value = 'Move failed — please try again'
}
}
return { jobs, loading, error, applied, survey, phoneScreen, interviewing, offer, hired, offerHired, rejected, fetchAll, move }
})