update ui design for all pages
This commit is contained in:
@@ -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 },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user