Files
krow_talent_app/src/pages/admin/TalentPool.jsx
Aravind 6249e00a3a
Some checks failed
CI / check (push) Failing after 4m58s
candidates and board ui agent issue
2026-09-05 10:46:06 +05:30

136 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useState } from 'react';
import { toast } from '@/components/ds';
import { useWorkerProfiles } from '@/lib/krowHooks';
import { AdminPage } from '@/components/admin/PageShell';
import { UiTreeRenderer } from '@/components/ui-tree/UiTreeRenderer';
import { useUiEditing } from '@/components/ui-tree/UiEditingProvider';
import { UiEditor } from '@/components/ui-editor/UiEditor';
import { composePage } from '@/lib/ui/composition';
import { SORTS } from '@/pages/admin/talent-pool/nodes';
import '@/components/ui-tree/nodeTypes';
import '@/pages/admin/talent-pool/nodes';
/**
* Admin Talent Pool — talent intelligence, not a card gallery.
*/
const BANDS = {
all: () => true,
elite: (s) => s >= 90,
excellent: (s) => s >= 75 && s < 90,
solid: (s) => s >= 60 && s < 75,
building: (s) => s > 0 && s < 60,
unscored: (s) => !s,
};
export default function AdminTalentPool() {
const { data: profiles = [], isLoading } = useWorkerProfiles();
const [search, setSearch] = useState('');
const [skill, setSkill] = useState('all');
const [experience, setExperience] = useState('all');
const [location, setLocation] = useState('all');
const [band, setBand] = useState('all');
const [availability, setAvailability] = useState('all');
const [sort, setSort] = useState('score');
const [saved, setSaved] = useState([]);
const skills = useMemo(
() => [...new Set(profiles.flatMap((p) => p.skills || []))].sort(),
[profiles]
);
const locations = useMemo(
() => [...new Set(profiles.map((p) => p.address).filter(Boolean))].sort(),
[profiles]
);
const availabilities = useMemo(
() => [...new Set(profiles.flatMap((p) => p.availability || []))].sort(),
[profiles]
);
const filtered = useMemo(() => {
const q = search.toLowerCase();
return profiles
.filter((p) => {
if (q && !`${p.full_name} ${p.email} ${p.desired_position} ${p.current_position} ${(p.skills || []).join(' ')}`.toLowerCase().includes(q)) return false;
if (skill !== 'all' && !(p.skills || []).includes(skill)) return false;
if (location !== 'all' && p.address !== location) return false;
if (availability !== 'all' && !(p.availability || []).includes(availability)) return false;
if (!BANDS[band](p.krow_score || 0)) return false;
if (experience === '5plus' && (p.experience_years || 0) < 5) return false;
if (experience === '2to5' && ((p.experience_years || 0) < 2 || (p.experience_years || 0) >= 5)) return false;
if (experience === 'under2' && (p.experience_years || 0) >= 2) return false;
return true;
})
.sort(SORTS[sort].compare);
}, [profiles, search, skill, experience, location, band, availability, sort]);
const isFiltered = Boolean(search) || [skill, experience, location, band, availability].some((v) => v !== 'all');
const clearFilters = () => {
setSearch(''); setSkill('all'); setExperience('all'); setLocation('all'); setBand('all'); setAvailability('all');
};
const segments = useMemo(() => {
const getBandMembers = (min, max) => profiles.filter((p) => {
const s = p.krow_score || 0;
return s >= min && (max == null || s < max);
});
return [
{ label: 'Elite', hint: '90+', members: getBandMembers(90), tone: 'brand' },
{ label: 'Excellent', hint: '75–89', members: getBandMembers(75, 90), tone: 'success' },
{ label: 'Solid', hint: '60–74', members: getBandMembers(60, 75), tone: 'default' },
{ label: 'Building', hint: 'under 60', members: getBandMembers(1, 60), tone: 'warning' },
{ label: 'Unscored', hint: 'no score', members: profiles.filter((p) => !(p.krow_score > 0)), tone: 'risk' },
].map((s) => ({
...s,
count: s.members.length,
available: s.members.filter((p) => (p.availability || []).length > 0).length,
avgExperience: (() => {
/* 0 years is "not stated", not "first year" — the cards that show it
gate on experience_years > 0, so the average is over the same set. */
const stated = s.members.filter((p) => (p.experience_years || 0) > 0);
return stated.length
? Math.round(stated.reduce((sum, p) => sum + p.experience_years, 0) / stated.length)
: 0;
})(),
}));
}, [profiles]);
const toggleSave = (id, name) => {
setSaved((prev) => {
const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id];
toast.success(prev.includes(id) ? `${name} removed from saved` : `${name} saved`);
return next;
});
};
const context = useMemo(() => ({
profiles, segments, filtered, isLoading, isFiltered, clearFilters, saved, toggleSave,
search, setSearch, skill, setSkill, experience, setExperience, location, setLocation,
band, setBand, availability, setAvailability, sort, setSort,
skills, locations, availabilities,
}), [
profiles, segments, filtered, isLoading, isFiltered, saved,
search, skill, experience, location, band, availability, sort, skills, locations, availabilities,
]);
return (
<AdminPage
title="Talent Pool"
subtitle="Discover and manage high-potential talent for future hiring."
>
<UiEditor />
<TalentPoolComposition context={context} />
</AdminPage>
);
}
/** The composed page. */
function TalentPoolComposition({ context }) {
const editing = useUiEditing();
const tree = editing?.tree || composePage('talent-pool').tree;
return <UiTreeRenderer nodes={tree} context={context} />;
}