Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/careers.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/lib/careers.ts |
| Original SHA-256 | bdbcf64f5b49f98cbaaf868fd102057e1a3b8aa113db9535fa30217b129fca07 |
| Git blob SHA | 1f26b8fa89d6689e0f319b58e30e61e8a4752ec1 |
| Reading-copy SHA-256 | bdbcf64f5b49f98cbaaf868fd102057e1a3b8aa113db9535fa30217b129fca07 |
Recovered source transcript
Download complete source text ↓206 lines · 6.6 KB
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { request } from './api'
// THE CAREERS SURFACE: published job postings, the applications they bring in, and interview self scheduling.
//
// Two audiences again, and the split matters more here than anywhere else in the product. The tenant side is
// behind a session. The PUBLIC side is the only unauthenticated path that touches tenant data, it takes no
// cookie, and it is reached from a separate entry point rather than from the signed in shell. Nothing in this
// file talks to the public routes: those live in the public entry, so a bug here cannot widen them.
export interface ManagedPosting {
id: string
public_token: string
requisition_id: string
title: string
status: string
published_at?: string
closes_at?: string
applications: number
created_at: string
}
export interface PostingInput {
requisition_id: string
title: string
summary: string
responsibilities: string
qualifications: string
location_text: string
remote_allowed: boolean
employment_type: string
clearance_text: string
pay_range_text: string
closes_at?: string | null
}
export interface InboundApplication {
id: string
posting_id: string
posting_title: string
full_name: string
email: string
phone?: string
location_text?: string
cover_note?: string
clearance_self_reported?: string
has_resume: boolean
status: string
status_reason?: string
candidate_id?: string
source: string
consented_at?: string
created_at: string
}
export interface ManagedOffer {
id: string
public_token: string
status: string
kind: string
expires_at: string
candidate_name: string
role: string
times_available: number
}
export interface OfferInput {
application_id: string
kind: string
round: number
duration_mins: number
location_text: string
interviewer: string
message: string
expires_in_days: number
slots: string[]
}
export const careersApi = {
postings: () => request<{ postings: ManagedPosting[]; count: number }>('/job-postings'),
createPosting: (v: PostingInput) =>
request<{ id: string; public_token: string; note: string }>('/job-postings', {
method: 'POST',
body: JSON.stringify(v),
}),
setPostingStatus: (id: string, status: string) =>
request<{ status: string }>(`/job-postings/${encodeURIComponent(id)}/status`, {
method: 'POST',
body: JSON.stringify({ status }),
}),
applications: () => request<{ applications: InboundApplication[]; count: number }>('/job-applications'),
review: (id: string, status: string, reason: string) =>
request<{ status: string }>(`/job-applications/${encodeURIComponent(id)}/review`, {
method: 'POST',
body: JSON.stringify({ status, reason }),
}),
convert: (id: string) =>
request<{ candidate_id: string; status: string }>(
`/job-applications/${encodeURIComponent(id)}/convert`,
{ method: 'POST' },
),
offers: () => request<{ offers: ManagedOffer[]; count: number }>('/interview-offers'),
createOffer: (v: OfferInput) =>
request<{ id: string; public_token: string; note: string }>('/interview-offers', {
method: 'POST',
body: JSON.stringify(v),
}),
withdrawOffer: (id: string) =>
request<{ withdrawn: boolean }>(`/interview-offers/${encodeURIComponent(id)}/withdraw`, {
method: 'POST',
}),
}
const K = {
postings: ['careers', 'postings'] as const,
applications: ['careers', 'applications'] as const,
offers: ['careers', 'offers'] as const,
}
export function usePostings() {
return useQuery({ queryKey: K.postings, queryFn: careersApi.postings })
}
export function useCreatePosting() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: PostingInput) => careersApi.createPosting(v),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.postings }),
})
}
export function useSetPostingStatus() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { id: string; status: string }) => careersApi.setPostingStatus(v.id, v.status),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.postings }),
})
}
// The inbound queue is refetched on a timer because it is fed by strangers rather than by anybody in the
// building. A recruiter watching the queue after a posting goes live should see an application arrive
// without reloading, and a minute is often enough to matter on a same day requisition.
export function useInboundApplications() {
return useQuery({
queryKey: K.applications,
queryFn: careersApi.applications,
refetchInterval: 60_000,
})
}
export function useReviewApplication() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { id: string; status: string; reason: string }) =>
careersApi.review(v.id, v.status, v.reason),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.applications }),
})
}
export function useConvertApplication() {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => careersApi.convert(id),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: K.applications })
void qc.invalidateQueries({ queryKey: K.postings })
// Converting creates a candidate, so every bench list is stale.
void qc.invalidateQueries({ queryKey: ['candidates'] })
},
})
}
export function useInterviewOffers() {
return useQuery({ queryKey: K.offers, queryFn: careersApi.offers })
}
export function useCreateOffer() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: OfferInput) => careersApi.createOffer(v),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.offers }),
})
}
export function useWithdrawOffer() {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => careersApi.withdrawOffer(id),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.offers }),
})
}
// APPLICATION_STATUSES are the review decisions, with what each one MEANS for the person who applied. The
// wording is deliberate: an application is a person, and "rejected" with no further thought is how a
// recruiting product becomes something people dread hearing from.
export const APPLICATION_STATUSES = [
{ value: 'new', label: 'New', hint: 'not looked at yet' },
{ value: 'reviewing', label: 'Reviewing', hint: 'somebody is reading it' },
{ value: 'accepted', label: 'Worth pursuing', hint: 'ready to become a candidate record' },
{ value: 'declined', label: 'Not a fit', hint: 'a reason is recorded, and it stays on the application' },
{ value: 'duplicate', label: 'Already known', hint: 'this person is already in the records' },
]