fix error line

This commit is contained in:
2026-08-17 17:33:56 +05:30
parent 69b38abced
commit 7fa21a4513
105 changed files with 1296 additions and 323 deletions

View File

@@ -5,7 +5,7 @@ import {
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Avatar, Badge, Button, Drawer, EmptyState, ProgressBar, SearchInput, SegmentedToggle,
Avatar, Badge, Button, Drawer, EmptyState, SearchInput, SegmentedToggle,
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, StatusBadge, toast,
} from '@/components/ds';
import {
@@ -59,7 +59,7 @@ const SORTS = {
starved: { label: 'Fewest applicants', compare: (a, b) => a.stats.applied - b.stats.applied },
updated: {
label: 'Recently updated',
compare: (a, b) => new Date(b.updated_date || b.created_date) - new Date(a.updated_date || a.created_date),
compare: (a, b) => new Date(b.updated_date || b.created_date).getTime() - new Date(a.updated_date || a.created_date).getTime(),
},
title: { label: 'Title A–Z', compare: (a, b) => a.title.localeCompare(b.title) },
};
@@ -72,7 +72,8 @@ const HEALTH_TONE = {
};
/** Hiring health as a small filled chip — it is a state, so it reads as one. */
function HealthChip({ health, className }) {
/** @param {any} props */
function HealthChip({ health, className = '' }) {
return (
<span
className={cn(
@@ -740,43 +741,48 @@ function PositionDrawer({ position, onClose, onViewCandidates, onOpenPosition })
</section>
)}
{/* Hiring status */}
<section>
<SectionTitle title="Hiring status" />
<dl className="grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-3">
{[
{ label: 'Applicants', value: s.applied },
{ label: 'Shortlisted', value: s.shortlisted },
{ label: 'Interviews', value: s.interviews },
{ label: 'Hired', value: s.hired },
{ label: 'Qualified (70+)', value: s.qualified },
{ label: 'Avg score', value: s.avgScore || '—' },
].map((m) => (
<div key={m.label}>
<dt className="text-[10px] font-medium uppercase tracking-wide text-ink-4">{m.label}</dt>
<dd className="font-heading text-title font-bold tabular-nums text-ink-1">{m.value}</dd>
</div>
))}
</dl>
<div className="mt-4">
<div className="mb-1.5 flex items-baseline justify-between">
<span className="text-caption text-ink-2">Screening progress</span>
<span className="text-caption font-semibold tabular-nums text-ink-1">{s.coverage}%</span>
</div>
<ProgressBar
value={s.coverage}
tone={s.coverage === 100 ? 'success' : s.coverage >= 60 ? 'brand' : 'warning'}
size="sm"
/>
<p className="mt-1 text-[11px] text-ink-4">
{s.applied - s.unscreened} of {s.applied} applicants scored
</p>
</div>
</section>
{/* Hiring progression — the single source of truth for the pipeline.
A separate "Hiring status" block used to sit above this one repeating
Shortlisted, Interviews and Hired as figures, which is exactly what the
progression already draws. Two readings of the same funnel invite the
reader to check whether they agree, so the duplicated stage counts are
gone and the three figures the progression does *not* carry — supply,
quality and the screening average — lead it as one line. */}
<section>
<SectionTitle title="Hiring progression" />
<dl className="mb-3 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-body-sm">
<div className="flex items-baseline gap-1.5">
<dt className="sr-only">Total applicants</dt>
<dd className="font-semibold tabular-nums text-ink-1">{s.applied}</dd>
<span className="text-ink-3">Applicant{s.applied === 1 ? '' : 's'}</span>
</div>
<span className="text-ink-4" aria-hidden="true">·</span>
<div className="flex items-baseline gap-1.5">
<dt className="sr-only">Qualified, scoring 70 or above</dt>
<dd className="font-semibold tabular-nums text-ink-1">{s.qualified}</dd>
<span className="text-ink-3">Qualified (70+)</span>
</div>
<span className="text-ink-4" aria-hidden="true">·</span>
<div className="flex items-baseline gap-1.5">
<dt className="text-ink-3">Avg Score</dt>
<dd className="font-semibold tabular-nums text-ink-1">{s.avgScore || '—'}</dd>
</div>
{/* Screening coverage was the one figure in the old block that the
progression cannot show — a stage count says how many reached
Screened, not what share of the intake that is. */}
{s.applied > 0 && (
<>
<span className="text-ink-4" aria-hidden="true">·</span>
<div className="flex items-baseline gap-1.5">
<dt className="sr-only">Screening coverage</dt>
<dd className="font-semibold tabular-nums text-ink-1">{s.coverage}%</dd>
<span className="text-ink-3">screened</span>
</div>
</>
)}
</dl>
<Progression steps={p.progression} />
</section>
@@ -811,22 +817,45 @@ function PositionDrawer({ position, onClose, onViewCandidates, onOpenPosition })
))}
</ul>
) : (
<p className="text-body-sm text-ink-3">
Nobody has been screened for this role yet, so there is no ranking to show.
</p>
/* Two short lines, not a paragraph. The reason nobody is ranked is
always the same — screening has not run — so it needs stating once,
plainly, rather than explained. */
<div className="rounded-lg border border-dashed border-border px-3 py-4">
<p className="text-body-sm font-medium text-ink-2">No screened candidates yet</p>
<p className="mt-0.5 text-caption leading-relaxed text-ink-3">
Candidates will appear here once screening is completed.
</p>
</div>
)}
</section>
{/* The timeline sits beside Top candidates, so it earns a rail and nodes:
two plain label/date lists side by side read as one table split in
half. The dots say "this one is chronological" without a heavier
treatment. */}
<section>
<SectionTitle title="Hiring timeline" />
<ol className="space-y-2">
<ol className="space-y-0">
{timeline.map((e, i) => (
<li key={i} className="flex items-baseline justify-between gap-3 text-body-sm">
<span className="text-ink-2">{e.label}</span>
<span className="shrink-0 text-caption text-ink-4">
{e.date
? new Date(e.date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
: '—'}
<li key={i} className="flex gap-2.5 pb-3 last:pb-0">
<span className="flex flex-col items-center" aria-hidden="true">
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
i === timeline.length - 1 ? 'bg-krow-blue' : 'bg-ink-4'
)}
/>
{i < timeline.length - 1 && <span className="mt-1 w-px flex-1 bg-border" />}
</span>
<span className="min-w-0 flex-1">
<span className="block text-body-sm leading-snug text-ink-2">{e.label}</span>
<span className="block text-caption text-ink-4">
{e.date
? new Date(e.date).toLocaleDateString(undefined, {
month: 'short', day: 'numeric', year: 'numeric',
})
: '—'}
</span>
</span>
</li>
))}
@@ -841,7 +870,7 @@ function PositionDrawer({ position, onClose, onViewCandidates, onOpenPosition })
{p.applications.length ? (
<ol className="space-y-2">
{[...p.applications]
.sort((a, b) => new Date(b.updated_date) - new Date(a.updated_date))
.sort((a, b) => new Date(b.updated_date).getTime() - new Date(a.updated_date).getTime())
.slice(0, 5)
.map((a) => (
<li key={a.id} className="flex items-baseline justify-between gap-3 text-body-sm">
@@ -1161,7 +1190,8 @@ export default function AdminPositions() {
}
/** A compact labelled select pill — consistent on every Admin management page. */
export function FilterSelect({ value, onChange, label, options, className }) {
/** @param {any} props */
export function FilterSelect({ value, onChange, label, options, className = '' }) {
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger