update ui design for all pages

This commit is contained in:
2026-08-12 18:56:56 +05:30
parent 25c4ee4a6a
commit 3beb66059b
58 changed files with 7892 additions and 1891 deletions

View File

@@ -8,7 +8,11 @@ import {
text, timeline,
} from '../blocks';
import { pct, plural, verb } from '../insights';
/* The proof-method wording is the Forge page's own, so a skill is never named
one thing in the library and another in the panel beside it. */
import { TYPE_LABEL } from '@/components/forge/challengeMeta';
import { toFICO } from '@/lib/talentHome';
import { PERMISSIONS, PROFILE_ACTIONS } from '@/pages/admin/permissions';
/**
* Admin capabilities — Control Center, Candidates Analysis, Activity.
@@ -1357,161 +1361,162 @@ export const ACTIVITY_CAPABILITIES = [
/* ── KROW Forge ─────────────────────────────────────────────────────────── */
/**
* Forge capabilities — workforce skill verification.
* Forge capabilities — managing workforce skill verification.
*
* The Forge questions are not hiring questions: they are about proof. What is
* verified, what can be proven next, and what is gating the rest. Every figure
* comes from `f.forge`, which is derived with the same unlock gate the page
* renders from.
* The Forge questions an administrator asks are not a worker's. A worker asks
* what to prove next; the person who owns the library asks what is in it, what
* is live, what Owliver is checking, and whether any of it is reaching the
* workforce. Every figure comes from `f.forge`, derived from the same Course
* records and worker profiles the page renders from.
*/
const forgeProgress = (f) => {
const forgeLibrary = (f) => {
const g = f.forge;
return doc(
heading('Forge progress', `${plural(g.courses.length, 'challenge')} in the library`),
kpis([
{ label: 'Verified skills', value: g.verified.length, sub: g.verified.length ? 'proven' : 'nothing proven yet' },
{ label: 'In progress', value: g.inProgress.length, sub: 'challenges started' },
{ label: 'Ready to start', value: g.ready.length, sub: 'unlocked now' },
{ label: 'XP available', value: g.xpAvailable, sub: `across ${plural(g.courses.length - g.completed.length, 'challenge')}` },
]),
g.verified.length
? list(g.verified.map((b) => `**${b.name}** — ${b.level || 'Standard'}, verified`))
: text('No skill is verified yet. The first one comes from completing a challenge and submitting proof for Owliver to grade.'),
g.byDifficulty.length ? heading('Library by difficulty') : null,
meters(g.byDifficulty.map((d) => ({
label: d.level.charAt(0).toUpperCase() + d.level.slice(1),
value: d.count,
max: g.courses.length,
}))),
g.locked.length
? note(`${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} still gated by real-work requirements.`)
: null
);
};
const forgeNextChallenge = (f) => {
const g = f.forge;
if (!g.next) return answer('Every active challenge is complete — there is nothing left to start.');
const c = g.next;
const gate = g.blocked.find((b) => b.course.id === c.id);
return doc(
heading(c.title, `${c.proof_skill || c.category} · ${c.difficulty}`),
text(c.description),
kpis([
{ label: 'Duration', value: `${c.estimated_minutes} min` },
{ label: 'XP', value: c.xp },
{ label: 'Earns', value: c.badge_reward || '—' },
{ label: 'State', value: gate ? 'Locked' : 'Ready', tone: gate ? 'warning' : 'success' },
]),
gate
? insights([{
tone: 'warning',
title: 'Blocked for now',
body: `${gate.reasons.join('. ')}. Those are real-work requirements, so they clear through shifts rather than through study.`,
}])
: insights([{
tone: 'success',
title: 'Ready to start',
body: `Nothing is gating this one. ${plural(c.estimated_minutes, 'minute')} of work is worth ${c.xp} XP${c.badge_reward ? ` and the ${c.badge_reward} badge` : ''}.`,
}]),
g.ready.length > 1
? actions(g.ready.slice(1, 3).map((alt) => ({
title: alt.title,
body: `${alt.difficulty} · ${alt.estimated_minutes} min · ${alt.xp} XP. An alternative if this one does not fit today.`,
})))
: null
);
};
const forgeBlockers = (f) => {
const g = f.forge;
if (!g.locked.length) {
return answer(`Nothing is locked — all ${plural(g.ready.length, 'remaining challenge')} can be started now.`);
if (!g.library.length) {
return answer('The library is empty. A skill defines what the workforce learns, how they prove it, and what Owliver checks — start one from Add Skill Training.');
}
return doc(
heading(`${plural(g.locked.length, 'challenge')} blocked`, 'and what clears each one'),
table(
[
{ key: 'challenge', label: 'Challenge' },
{ key: 'skill', label: 'Skill' },
{ key: 'requirement', label: 'Blocked by' },
],
g.blocked.map(({ course, reasons }) => ({
challenge: course.title,
skill: course.proof_skill || course.category,
requirement: reasons[0] || '—',
}))
),
insights(g.blocked.slice(0, 2).map(({ course, reasons }) => ({
tone: 'warning',
title: course.title,
body: `${reasons.join('. ')}. Worth ${course.xp} XP${course.badge_reward ? ` and the ${course.badge_reward} badge` : ''} once it opens.`,
heading('Skill library', `${plural(g.library.length, 'skill')} defined`),
kpis([
{ label: 'Active skills', value: g.published.length, sub: `of ${g.library.length} in the library` },
{ label: 'Training modules', value: g.withTraining.length, sub: 'carrying material' },
{ label: 'Challenges', value: g.withChallenge.length, sub: 'evidence-based' },
{ label: 'Verified skills', value: g.workforceVerified, sub: g.workforceVerified ? 'earned by the workforce' : 'none earned yet' },
]),
g.byCategory.length ? heading('By category') : null,
meters(g.byCategory.map((c) => ({
label: c.category,
value: c.count,
max: g.library.length,
}))),
note('Requirements are measured against the profile record, so they update as shifts and reliability are logged.')
g.drafts.length
? note(`${plural(g.drafts.length, 'skill')} ${verb(g.drafts.length, 'is', 'are')} still in draft and not visible to the workforce.`)
: null
);
};
const forgeReadyNow = (f) => {
const forgePublished = (f) => {
const g = f.forge;
if (!g.ready.length) {
if (!g.published.length) {
return answer(`Nothing is published. ${g.drafts.length ? `${plural(g.drafts.length, 'draft')} ${verb(g.drafts.length, 'is', 'are')} waiting on a decision.` : 'The library has no skills yet.'}`);
}
return doc(
heading(`${plural(g.published.length, 'skill')} published`, 'live to the workforce'),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'category', label: 'Category' },
{ key: 'proof', label: 'Proof' },
{ key: 'minutes', label: 'Min', align: 'right' },
],
g.published.map((c) => ({
skill: c.title,
category: c.category || '—',
proof: TYPE_LABEL[c.challenge?.type] || 'Not set',
minutes: c.estimated_minutes || '—',
}))
),
g.drafts.length
? insights([{
tone: 'warning',
title: `${plural(g.drafts.length, 'skill')} in draft`,
body: `${g.drafts.map((c) => c.title).join(', ')}. A draft is invisible to the workforce until it is published.`,
}])
: note('Every skill in the library is published.')
);
};
const forgeEvaluation = (f) => {
const g = f.forge;
const scored = g.library.filter((c) => c.challenge?.rubric?.length);
if (!scored.length) {
return answer('No skill has evaluation criteria yet. Owliver scores a submission against the criteria set on the skill, so a skill without them cannot verify anything.');
}
return doc(
heading('What Owliver evaluates', `across ${plural(scored.length, 'skill')}`),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'criteria', label: 'Criteria' },
{ key: 'pass', label: 'Passes at', align: 'right' },
],
scored.map((c) => ({
skill: c.title,
criteria: c.challenge.rubric.map((r) => r.criterion).join(', '),
pass: c.pass_score || '—',
}))
),
g.library.length > scored.length
? insights([{
tone: 'warning',
title: 'Skills with nothing to score against',
body: `${g.library.filter((c) => !c.challenge?.rubric?.length).map((c) => c.title).join(', ')}. Owliver needs criteria to return a verdict.`,
}])
: note('Every skill in the library tells Owliver what to check.')
);
};
const forgeWorkforce = (f) => {
const g = f.forge;
const touched = g.usage.filter((u) => u.completed > 0 || u.verified > 0);
if (!touched.length) {
return doc(
text('Nothing is unlocked right now.'),
g.locked.length ? text(`${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} waiting on real-work requirements.`) : null
text('No skill in the library has been completed by anyone yet.'),
note('Completion and verification are read from worker profiles, so figures appear here as the workforce works through the library.')
);
}
return doc(
heading(`${plural(g.ready.length, 'challenge')} ready to start`),
table(
[
{ key: 'challenge', label: 'Challenge' },
{ key: 'difficulty', label: 'Difficulty' },
{ key: 'minutes', label: 'Min', align: 'right' },
{ key: 'xp', label: 'XP', align: 'right' },
],
g.ready.map((c) => ({
challenge: c.title,
difficulty: c.difficulty,
minutes: c.estimated_minutes,
xp: c.xp,
}))
),
text(
`Cheapest first: **${[...g.ready].sort((a, b) => a.estimated_minutes - b.estimated_minutes)[0].title}**. `
+ `Highest value: **${[...g.ready].sort((a, b) => b.xp - a.xp)[0].title}**.`
)
);
};
const forgeSkillMap = (f) => {
const g = f.forge;
const skills = g.courses.map((c) => ({
skill: c.proof_skill || c.category || 'Workforce skill',
state: g.verified.some((b) => b.course_id === c.id)
? 'Verified'
: g.locked.some((l) => l.id === c.id) ? 'Locked' : 'Ready',
xp: c.xp,
}));
return doc(
heading('Skills this library can verify', plural(skills.length, 'skill')),
heading('Workforce usage', `${plural(touched.length, 'skill')} in use`),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'state', label: 'State' },
{ key: 'xp', label: 'XP', align: 'right' },
{ key: 'completed', label: 'Completed', align: 'right' },
{ key: 'verified', label: 'Verified', align: 'right' },
],
skills
touched
.sort((a, b) => b.verified - a.verified || b.completed - a.completed)
.map((u) => ({
skill: u.course.title,
completed: u.completed,
verified: u.verified,
}))
),
text(
g.verified.length
? `${plural(g.verified.length, 'skill')} already carries evidence an employer can read.`
: 'None of these carry evidence yet. Each verified skill adds a record an employer can read against the profile.'
)
g.library.length > touched.length
? note(`${plural(g.library.length - touched.length, 'skill')} ${verb(g.library.length - touched.length, 'has', 'have')} no completions on record.`)
: null
);
};
const forgeGaps = (f) => {
const g = f.forge;
const gaps = [
...g.drafts.map((c) => ({ skill: c.title, gap: 'Unpublished draft' })),
...g.library.filter((c) => !c.challenge?.type).map((c) => ({ skill: c.title, gap: 'No proof method' })),
...g.library.filter((c) => !c.challenge?.rubric?.length).map((c) => ({ skill: c.title, gap: 'No Owliver criteria' })),
...g.library.filter((c) => !c.training_outline?.length && !c.quiz?.length).map((c) => ({ skill: c.title, gap: 'No training material' })),
];
if (!gaps.length) {
return answer(`All ${plural(g.library.length, 'skill')} carry training, a proof method and evaluation criteria, and every one is published.`);
}
return doc(
heading(`${plural(gaps.length, 'gap')} to close`, 'before these verify anything'),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'gap', label: 'Missing' },
],
gaps
),
note('A skill needs training, a way to submit evidence, and criteria for Owliver to score against before it can verify a workforce skill.')
);
};
@@ -1763,12 +1768,12 @@ const hiringRecentHires = (f) => {
export const respondForge = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'block', 'locked', 'lock', 'requirement', 'prerequisite', 'why can', 'unlock')) return forgeBlockers(f);
if (has(q, 'next', 'should i', 'which challenge', 'recommend', 'best')) return forgeNextChallenge(f);
if (has(q, 'ready', 'start now', 'available', 'can i start', 'active challenge')) return forgeReadyNow(f);
if (has(q, 'skill', 'verified', 'proof', 'evidence', 'badge')) return forgeSkillMap(f);
if (has(q, 'summar', 'progress', 'how am i', 'xp', 'overview')) return forgeProgress(f);
return forgeProgress(f);
if (has(q, 'publish', 'live', 'draft', 'unpublish', 'archived', 'status')) return forgePublished(f);
if (has(q, 'owliver', 'evaluat', 'criteri', 'rubric', 'grade', 'score against', 'pass')) return forgeEvaluation(f);
if (has(q, 'gap', 'missing', 'incomplete', 'unfinished', 'needs', 'ready to publish')) return forgeGaps(f);
if (has(q, 'workforce', 'usage', 'completed', 'verified', 'earned', 'badge', 'who has', 'uptake')) return forgeWorkforce(f);
if (has(q, 'skill', 'library', 'challenge', 'course', 'training', 'category', 'summar', 'overview', 'how many', 'what do we have')) return forgeLibrary(f);
return forgeLibrary(f);
};
export const respondTalentPool = (question, f) => {
@@ -1793,11 +1798,11 @@ export const respondHiredHistory = (question, f) => {
/* ── Capability manifests for the three new contexts ────────────────────── */
export const FORGE_CAPABILITIES = [
{ id: 'forge-progress', label: 'Forge Progress', icon: Gauge, run: forgeProgress },
{ id: 'forge-next', label: 'Next Challenge', icon: Target, run: forgeNextChallenge },
{ id: 'forge-ready', label: 'Ready To Start', icon: UserCheck, run: forgeReadyNow },
{ id: 'forge-blockers', label: 'What Is Blocking Me', icon: Lock, run: forgeBlockers },
{ id: 'forge-skills', label: 'Skill Map', icon: ShieldCheck, run: forgeSkillMap },
{ id: 'forge-library', label: 'Skill Library', icon: Gauge, run: forgeLibrary },
{ id: 'forge-published', label: 'Published Skills', icon: Target, run: forgePublished },
{ id: 'forge-evaluation', label: 'Owliver Evaluation', icon: ShieldCheck, run: forgeEvaluation },
{ id: 'forge-workforce', label: 'Workforce Usage', icon: UserCheck, run: forgeWorkforce },
{ id: 'forge-gaps', label: 'Gaps To Close', icon: Lock, run: forgeGaps },
];
export const TALENT_POOL_CAPABILITIES = [
@@ -1813,3 +1818,120 @@ export const HIRED_HISTORY_CAPABILITIES = [
{ id: 'hiring-patterns', label: 'What Stands Out', icon: Layers, run: hiringPatterns },
{ id: 'hiring-recent', label: 'Recent Hires', icon: ScrollText, run: hiringRecentHires },
];
/* ── Profile ────────────────────────────────────────────────────────────────
*
* The account page answers questions about the operator rather than the
* workforce: what this account may do, how it is secured, what it has been
* doing, and which switches are on. Everything below reads the same constants
* the page renders, so the panel can never describe a scope or a control that
* is not on screen.
*/
const profilePermissions = () => doc(
heading('Your permissions', `${PERMISSIONS.length} scopes on this account`),
table(
[{ key: 'scope', label: 'Scope' }, { key: 'level', label: 'Level' }, { key: 'detail', label: 'Covers' }],
PERMISSIONS.map((p) => ({
scope: p.scope,
level: {
value: p.level,
tone: p.level === 'Full access' ? 'success' : p.level === 'Read only' ? 'info' : 'muted',
},
detail: p.detail,
}))
),
note('Scopes are set by the account owner. Billing stays restricted to owners.')
);
const profileIdentity = (f) => {
const user = f.user || {};
return doc(
heading('This account'),
table(
[{ key: 'field', label: 'Field' }, { key: 'value', label: 'Value' }],
[
{ field: 'Name', value: user.full_name || '—' },
{ field: 'Email', value: user.email || '—' },
{ field: 'Role', value: 'Platform Administrator' },
{ field: 'Department', value: 'Workforce Operations' },
{ field: 'Account status', value: { value: 'Active · verified', tone: 'success' } },
{
field: 'Member since',
value: user.created_date
? new Date(user.created_date).toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
: '—',
},
]
),
text('Use **Edit profile** at the top of the page to change your display name. Email and role are set by the account owner.')
);
};
const profileSecurity = () => doc(
heading('Security on this account'),
status([
{ label: 'Two-factor authentication', ok: true, value: 'On', note: 'Required for privileged actions.' },
{ label: 'Password age', ok: true, value: '42 days', note: 'Administrator passwords expire every 90 days.' },
{ label: 'Session', ok: true, value: 'Browser tab', note: 'Closing the tab signs this account out.' },
]),
text('**Change password** and **Manage sessions** sit in the Security card on the right.')
);
const profilePreferences = (f) => {
const p = f.user?.preferences || {};
const state = (on) => ({ value: on ? 'On' : 'Off', tone: on ? 'success' : 'muted' });
return doc(
heading('Your preferences'),
table(
[{ key: 'setting', label: 'Setting' }, { key: 'state', label: 'State' }, { key: 'detail', label: 'Effect' }],
[
{ setting: 'Owliver workspace', state: state(p.owliverDefault !== false), detail: 'Opens this panel by default on supported pages' },
{ setting: 'Compact density', state: state(Boolean(p.compactDensity)), detail: 'Tighter row heights across Admin tables' },
{ setting: 'Email digest', state: state(p.emailDigest !== false), detail: 'A daily summary of what needs attention' },
]
),
note('Preferences save to your account as soon as you flip a switch.')
);
};
const profileActivity = (f) => {
const email = f.user?.email;
const mine = f.activity.filter((e) => e.user_email === email).slice(0, 6);
if (!mine.length) {
return doc(text('No activity is recorded against this account yet.'));
}
return doc(
heading('Your recent activity', `${plural(mine.length, 'event')} on this account`),
timeline(mine.map((e) => ({
title: e.event_type.replace(/_/g, ' '),
description: e.details || undefined,
timestamp: new Date(e.created_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
})))
);
};
const profileActions = () => doc(
heading('What you can do here'),
list(PROFILE_ACTIONS.map((a) => `**${a.label}** — ${a.detail}`)),
note('Everything above is on this page. Nothing here changes another operator’s account.')
);
export const respondProfile = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'permission', 'scope', 'access', 'allowed', 'can i')) return profilePermissions(f);
if (has(q, 'password', 'two-factor', 'two factor', '2fa', 'security', 'session', 'sign out', 'log out')) return profileSecurity(f);
if (has(q, 'preference', 'setting', 'digest', 'density', 'workspace', 'owliver')) return profilePreferences(f);
if (has(q, 'activity', 'recent', 'history', 'what have i')) return profileActivity(f);
if (has(q, 'do here', 'change my', 'edit', 'update', 'how do i')) return profileActions(f);
return profileIdentity(f);
};
export const PROFILE_CAPABILITIES = [
{ id: 'profile-permissions', label: 'My Permissions', icon: ShieldCheck, run: profilePermissions },
{ id: 'profile-identity', label: 'Account Details', icon: Users, run: profileIdentity },
{ id: 'profile-security', label: 'Security', icon: Lock, run: profileSecurity },
{ id: 'profile-preferences', label: 'My Preferences', icon: Gauge, run: profilePreferences },
{ id: 'profile-activity', label: 'My Recent Activity', icon: ScrollText, run: profileActivity },
{ id: 'profile-actions', label: 'What I Can Do Here', icon: Lightbulb, run: profileActions },
];