position ui and owliver update

This commit is contained in:
2026-08-14 01:18:59 +05:30
parent 1c653264b5
commit cccada9bd2
29 changed files with 3718 additions and 1313 deletions

View File

@@ -140,13 +140,24 @@ export function resolveCandidate(question, pool = []) {
function reasonsFor(row) {
const out = [];
if (!row.match) {
return ['This position states no requirements this candidate can be scored against'];
}
for (const line of row.match.met) {
out.push(`✓ ${line.name} — ${line.heldLabel}`);
}
for (const line of row.match.gaps) {
out.push(`⚠ ${line.name} — ${line.heldLabel}, needs ${line.requiredLabel}`);
}
out.push(row.availability.available ? '✓ Available when this starts' : `✕ ${row.availability.reason}`);
/* Availability is only known for people the workforce system holds a record
for. For everyone else it is reported as unknown rather than as free. */
if (row.availability.known === false) {
out.push('Availability not on file');
} else {
out.push(row.availability.available ? '✓ Available when this starts' : `✕ ${row.availability.reason}`);
}
if (row.duration.note) out.push(`⚠ ${row.duration.note}`);
return out;
@@ -155,31 +166,23 @@ function reasonsFor(row) {
/**
* The record to open for a recommended person.
*
* Matching reads worker profiles, because that is where verified skill levels
* live. The candidate profile is addressed by *application* id, because that is
* what the page was built to show. So the link is only real when this person has
* an application on file — and when they do not, the card says so rather than
* pointing somewhere that is not their record.
* One id, decided in one place. `poolFor` resolved this row against the
* Candidates dataset before it was ever ranked, so `candidateId` is the id of
* an application that genuinely exists — never a name, a rank, or a record
* invented to make the link work. A row without one never reaches here, because
* a person with no candidate record is not returned as a match at all.
*
* The position travels with the link so the profile knows what the person was
* being considered for.
* The route is the existing candidate route, opening the existing full profile.
* The position travels with it so the profile knows what the person was being
* considered for.
*/
export function candidateRoute(row, applications = [], position = null) {
const email = String(row.email || '').toLowerCase();
if (!email) return null;
const mine = applications.filter((a) => String(a.email || '').toLowerCase() === email);
if (!mine.length) return null;
/* An application for *this* role is the right record to open; failing that,
their most recent one, which is still their profile. */
const forThis = position && mine.find((a) => a.job_posting_id === position.id);
const chosen = forThis
|| [...mine].sort((a, b) => new Date(b.created_date) - new Date(a.created_date))[0];
const id = row?.candidateId;
if (!id) return null;
return position
? `/admin/candidates/${chosen.id}?for=${encodeURIComponent(position.id)}`
: `/admin/candidates/${chosen.id}`;
? `/admin/candidates/${id}?for=${encodeURIComponent(position.id)}`
: `/admin/candidates/${id}`;
}
/**
@@ -189,15 +192,53 @@ export function candidateRoute(row, applications = [], position = null) {
* — met requirements, gaps, and the availability verdict — so a card can never
* read as a stronger endorsement than the match actually is.
*/
const candidateBlock = (row, index, { position = null, inspectable = true } = {}) => insights([{
tone: row.strong ? 'success' : row.availability.available ? 'info' : 'warning',
title: `${index}. ${row.name} — ${row.score}% match`,
body: reasonsFor(row).join(' · '),
/* Clicking inspects the person *here*. Opening their full record is offered
separately, from the detail view, because leaving the panel loses the
position the admin is working on. */
prompt: inspectable && position ? `Show ${row.name} for ${position.title}` : null,
}]);
const candidateBlock = (row, index, { position = null, applications = [], inspectable = true } = {}) => {
/**
* Two different things a reader wants from a match, kept apart.
*
* **View Profile** opens that person's record on the Candidates page. It is an
* explicit control rather than the whole card, so reading the match reasons
* cannot navigate away by accident — and it resolves for everyone, applied or
* not, because the profile page reads both kinds of record.
*
* **The card itself** still asks Owliver about this person for this role, which
* is the existing inspect-and-assign conversation, unchanged.
*
* The hint stays informational: it explains what opening the profile will and
* will not show, and is no longer the only way in.
*/
const to = candidateRoute(row, applications, position);
/**
* The headline says what kind of evidence the number rests on.
*
* A skill match is scored on training this person completed; a requirement
* match is scored on what their application states. Printing both as "match"
* would make the weaker claim borrow the authority of the stronger one, and a
* candidate with nothing to score says so rather than showing a 0.
*/
const headline = !row.scored
? 'not scored'
: row.basis === 'verified'
? `${row.score}% match`
: `${row.score}% requirement fit`;
return insights([{
tone: row.strong ? 'success' : row.scored ? 'info' : 'neutral',
title: `${index}. ${row.name} — ${headline}`,
body: reasonsFor(row).join(' · '),
prompt: !inspectable || !position ? null : `Show ${row.name} for ${position.title}`,
action: to ? { label: 'View Profile', to } : null,
/* Two informational states, never a control: what the score rests on, and
whether they have applied for this role. The profile opens either way. */
hint: [
row.basis === 'stated'
? 'Scored from their application — no verified training on file.'
: !row.scored ? 'Insufficient profile data to score against this role.' : null,
row.applied ? null : 'No application for this role yet — assigning them creates one.',
].filter(Boolean).join(' ') || null,
}]);
};
/**
* The people who could do this work, ranked.
@@ -213,23 +254,31 @@ export function candidateMatches(position, context) {
if (!pool.length) {
return {
doc: doc(
text(`No worker profiles can be scored against **${position.title}** — it defines no skill requirements, or no profiles carry the skills it asks for.`),
note('Matching reads verified skill levels from worker profiles. Applicants without a profile appear in the pipeline but cannot be scored.')
text(`There are no candidates on file to score against **${position.title}**.`),
note('Candidates come from the Candidates page. Once somebody applies — to this role or any other — they can be scored against this position.')
),
};
}
/* Committed elsewhere is a workforce fact, so it can only exclude somebody the
workforce system holds a record for. A candidate with no such record is
ranked below the verified ones, never filtered out by an availability
nobody has recorded. */
const eligible = pool.filter((r) => r.availability.available);
const blocked = pool.filter((r) => !r.availability.available);
const shown = eligible.slice(0, 5);
const verified = pool.filter((r) => r.basis === 'verified').length;
return {
doc: doc(
heading(`Best matches for ${position.title}`,
`${status.strong.length} strong · ${eligible.length} available · ${pool.length} scored`),
`${pool.length} candidate${pool.length === 1 ? '' : 's'} · ${verified} with verified skills · ${status.strong.length} strong`),
...(shown.length
? shown.map((row, i) => candidateBlock(row, i + 1, { applications: context.applications, position }))
: [text('Nobody is currently free for this role.')]),
pool.length > shown.length + blocked.length
? note(`${pool.length - shown.length - blocked.length} more candidate${pool.length - shown.length - blocked.length === 1 ? '' : 's'} on file — ask for the full list or open Candidates to see them all.`)
: null,
blocked.length
? note(`${blocked.length} other ${blocked.length === 1 ? 'person is' : 'people are'} qualified but committed elsewhere — ask who is available next to see when they free up.`)
: null,
@@ -250,7 +299,7 @@ export function candidateMatches(position, context) {
export function availability(position, context) {
const pool = poolFor(position, context);
if (!pool.length) {
return { doc: doc(text(`No worker profiles can be assessed for **${position.title}**.`)) };
return { doc: doc(text(`There are no candidates on file to assess for **${position.title}**.`)) };
}
return {