Files
krow_talent_app/src/components/ds/StatusBadge.tsx

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>
);
}