import type { RRequisition } from '../../lib/types' // The transition graph, mirrored from internal/modules/recruiting/requisition_status.go. // // Duplicated deliberately, and kept honest by the server rather than by discipline: the buttons offered // here are only a convenience, and the server refuses anything illegal regardless of what was clicked. // Showing a recruiter a button that always fails is worse than showing fewer buttons. export const REQ_TRANSITIONS: Record = { draft: ['open', 'cancelled'], open: ['on_hold', 'filled', 'closed', 'cancelled'], on_hold: ['open', 'filled', 'closed', 'cancelled'], filled: ['open', 'closed'], closed: ['open'], cancelled: ['open'], } export const STATUS_LABEL: Record = { draft: 'Draft', open: 'Open', on_hold: 'On hold', filled: 'Filled', closed: 'Closed', cancelled: 'Cancelled', } // The verb, not the noun. A button that says "On hold" reads as a label; one that says "Put on hold" // reads as the action it performs. export const STATUS_ACTION: Record = { draft: 'Return to draft', open: 'Reopen', on_hold: 'Put on hold', filled: 'Mark filled', closed: 'Close', cancelled: 'Cancel', } export function statusColor(status: string): string { switch (status) { case 'open': return 'var(--color-ok)' case 'on_hold': return 'var(--color-warn)' case 'filled': return 'var(--color-blue-soft)' case 'draft': return 'var(--color-faint)' default: return 'var(--color-muted)' } } export const PRIORITY_LABEL: Record = { low: 'Low', normal: 'Normal', high: 'High', critical: 'Critical', } export function priorityColor(p: string): string { switch (p) { case 'critical': return 'var(--color-risk)' case 'high': return 'var(--color-warn)' default: return 'var(--color-faint)' } } // A rate range, rendered from the decimal strings the API sends. // // The string is sliced, never parsed to a number. Number() would round a value that was stored exactly, // and the whole point of carrying money as a decimal string is that it survives the trip unchanged. // // Always two decimal places. numeric(12,4) sends 168.5000, and trimming trailing zeros renders that as // "$168.5", which reads as a typo rather than as money. Extra precision beyond cents is kept if it is // really there, because a rate carrying four places is a rate someone entered that way on purpose. export function rateRange(min?: string, max?: string, suffix = '/hr'): string { const fmt = (v: string) => { const [whole, frac = ''] = v.split('.') const trimmed = frac.replace(/0+$/, '') return '$' + whole + '.' + (trimmed.length <= 2 ? trimmed.padEnd(2, '0') : trimmed) } if (min && max) return `${fmt(min)} to ${fmt(max)}${suffix}` if (min) return `from ${fmt(min)}${suffix}` if (max) return `up to ${fmt(max)}${suffix}` return '' } // How a job order's target date should read. Absent is not the same as zero, and a passed target must // say so plainly rather than showing a negative number. export function targetText(r: RRequisition): { text: string; urgent: boolean } | null { if (r.days_to_target === undefined || r.days_to_target === null) return null const d = r.days_to_target if (d < 0) return { text: `${Math.abs(d)} day${Math.abs(d) === 1 ? '' : 's'} past target`, urgent: true } if (d === 0) return { text: 'Target is today', urgent: true } return { text: `${d} day${d === 1 ? '' : 's'} to target`, urgent: d <= 7 } }