113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import type { DsProps } from './props';
|
|
import * as React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
/**
|
|
* The domain's states, mapped to a tone and a human label in one place.
|
|
*
|
|
* This map is the reason StatusBadge exists: `ai_screened` appeared with three
|
|
* different colours and two different spellings across the reference pages.
|
|
* Adding a state here makes it correct everywhere at once.
|
|
*/
|
|
export const STATUS_MAP = {
|
|
/* Application lifecycle */
|
|
applied: { label: 'Applied', tone: 'neutral' },
|
|
ai_screened: { label: 'AI Screened', tone: 'info' },
|
|
shortlisted: { label: 'Shortlisted', tone: 'accent' },
|
|
interview: { label: 'Interviewing', tone: 'warning' },
|
|
hired: { label: 'Hired', tone: 'success' },
|
|
rejected: { label: 'Declined', tone: 'destructive' },
|
|
|
|
/* Job posting status */
|
|
active: { label: 'Hiring', tone: 'default' },
|
|
draft: { label: 'Draft', tone: 'neutral' },
|
|
paused: { label: 'Paused', tone: 'warning' },
|
|
closed: { label: 'Closed', tone: 'destructive' },
|
|
|
|
/* Staff status */
|
|
onboarding: { label: 'Onboarding', tone: 'warning' },
|
|
inactive: { label: 'Inactive', tone: 'neutral' },
|
|
|
|
/* Score bands — the same vocabulary talentHome.js uses */
|
|
elite: { label: 'Elite', tone: 'default' },
|
|
excellent: { label: 'Excellent', tone: 'success' },
|
|
solid: { label: 'Solid', tone: 'info' },
|
|
building: { label: 'Building', tone: 'warning' },
|
|
new: { label: 'New', tone: 'neutral' },
|
|
no_score: { label: 'No score yet', tone: 'neutral' },
|
|
|
|
/* Interview verdicts */
|
|
hire: { label: 'Hire', tone: 'success' },
|
|
maybe: { label: 'Maybe', tone: 'warning' },
|
|
no: { label: 'No', tone: 'destructive' },
|
|
|
|
/* Evidence verdicts */
|
|
verified: { label: 'Verified', tone: 'success' },
|
|
needs_work: { label: 'Needs work', tone: 'warning' },
|
|
failed: { label: 'Failed', tone: 'destructive' },
|
|
pending: { label: 'Pending', tone: 'neutral' },
|
|
expired: { label: 'Expired', tone: 'destructive' },
|
|
|
|
/* Profile tiers */
|
|
Skilled: { label: 'Skilled', tone: 'default' },
|
|
'Cross-Trained': { label: 'Cross-Trained', tone: 'info' },
|
|
Beginner: { label: 'Beginner', tone: 'neutral' },
|
|
};
|
|
|
|
/** Title-cases an unknown status so it never renders as a raw enum. */
|
|
const humanize = (status) =>
|
|
String(status).replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
|
|
const DOT_COLOR = {
|
|
default: 'bg-krow-blue',
|
|
soft: 'bg-krow-blue',
|
|
accent: 'bg-krow-blue',
|
|
navy: 'bg-krow-navy',
|
|
success: 'bg-success',
|
|
warning: 'bg-warning',
|
|
destructive: 'bg-destructive',
|
|
info: 'bg-info',
|
|
neutral: 'bg-ink-4',
|
|
outline: 'bg-ink-4',
|
|
};
|
|
|
|
/**
|
|
* StatusBadge — renders a domain status with its canonical colour and wording.
|
|
*
|
|
* `dot` adds a leading indicator for dense rows where colour alone is doing too
|
|
* much work; it also gives colour-blind users a second cue alongside the label.
|
|
*/
|
|
/** @param {any} props */
|
|
export function StatusBadge({ status, label, tone, dot = false, size = 'default', className, ...props }: DsProps) {
|
|
const { t, i18n } = useTranslation();
|
|
const entry = STATUS_MAP[status] || {};
|
|
const resolvedTone = tone || entry.tone || 'neutral';
|
|
|
|
/* STATUS_MAP stays the single list of statuses and their tones; the catalogue
|
|
holds the words. Translated by STATUS rather than by the English label, so
|
|
"Hired" the application state and "Hired" the badge on a worker can diverge
|
|
later without one of them silently changing the other.
|
|
An explicit `label` prop still wins — a caller passing its own wording has
|
|
already decided, and `i18n.exists` keeps an unknown status on the
|
|
humanised fallback instead of inventing a key for it. */
|
|
const key = `status.${status}`;
|
|
const resolvedLabel = label
|
|
|| (status && i18n.exists(key) ? t(key) : null)
|
|
|| entry.label
|
|
|| humanize(status);
|
|
|
|
return (
|
|
<Badge variant={resolvedTone} size={size} className={cn('gap-1.5', className)} {...props}>
|
|
{dot && (
|
|
<span
|
|
className={cn('w-1.5 h-1.5 rounded-full shrink-0', DOT_COLOR[resolvedTone])}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
{resolvedLabel}
|
|
</Badge>
|
|
);
|
|
}
|