update ui design for all pages
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Bookmark, Star, UserRound } from 'lucide-react';
|
||||
import { Bookmark, Star, UserRound, Trophy, CheckCircle2, TrendingUp, Sparkles } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Avatar, Badge, DataTable, IconButton, ProgressRing, SearchInput, toast,
|
||||
@@ -11,10 +11,6 @@ import { FilterSelect } from '@/pages/admin/Positions';
|
||||
|
||||
/**
|
||||
* Admin Talent Pool — talent intelligence, not a card gallery.
|
||||
*
|
||||
* These are people who have not applied to anything, so the job is discovery:
|
||||
* filter down, compare, shortlist. A dense list makes that possible; one card
|
||||
* per person makes it a scroll.
|
||||
*/
|
||||
|
||||
const SORTS = {
|
||||
@@ -33,6 +29,54 @@ const BANDS = {
|
||||
unscored: (s) => !s,
|
||||
};
|
||||
|
||||
const SEGMENT_CONFIG = {
|
||||
Elite: {
|
||||
key: 'elite',
|
||||
icon: Trophy,
|
||||
bg: 'bg-blue-50/80 text-blue-600 dark:bg-blue-950/60 dark:text-blue-400',
|
||||
border: 'border-blue-200/50 dark:border-blue-800/40 hover:border-blue-400',
|
||||
activeBorder: 'border-blue-600 ring-2 ring-blue-500/20',
|
||||
text: 'text-blue-600 dark:text-blue-400',
|
||||
accent: 'from-blue-500 to-indigo-500',
|
||||
},
|
||||
Excellent: {
|
||||
key: 'excellent',
|
||||
icon: Star,
|
||||
bg: 'bg-emerald-50/80 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400',
|
||||
border: 'border-emerald-200/50 dark:border-emerald-800/40 hover:border-emerald-400',
|
||||
activeBorder: 'border-emerald-600 ring-2 ring-emerald-500/20',
|
||||
text: 'text-emerald-600 dark:text-emerald-400',
|
||||
accent: 'from-emerald-500 to-teal-500',
|
||||
},
|
||||
Solid: {
|
||||
key: 'solid',
|
||||
icon: CheckCircle2,
|
||||
bg: 'bg-indigo-50/80 text-indigo-600 dark:bg-indigo-950/60 dark:text-indigo-400',
|
||||
border: 'border-indigo-200/50 dark:border-indigo-800/40 hover:border-indigo-400',
|
||||
activeBorder: 'border-indigo-600 ring-2 ring-indigo-500/20',
|
||||
text: 'text-indigo-600 dark:text-indigo-400',
|
||||
accent: 'from-indigo-500 to-purple-500',
|
||||
},
|
||||
Building: {
|
||||
key: 'building',
|
||||
icon: TrendingUp,
|
||||
bg: 'bg-amber-50/80 text-amber-600 dark:bg-amber-950/60 dark:text-amber-400',
|
||||
border: 'border-amber-200/50 dark:border-amber-800/40 hover:border-amber-400',
|
||||
activeBorder: 'border-amber-600 ring-2 ring-amber-500/20',
|
||||
text: 'text-amber-600 dark:text-amber-400',
|
||||
accent: 'from-amber-500 to-orange-500',
|
||||
},
|
||||
Unscored: {
|
||||
key: 'unscored',
|
||||
icon: Sparkles,
|
||||
bg: 'bg-orange-50/80 text-orange-600 dark:bg-orange-950/60 dark:text-orange-400',
|
||||
border: 'border-orange-200/50 dark:border-orange-800/40 hover:border-orange-400',
|
||||
activeBorder: 'border-orange-600 ring-2 ring-orange-500/20',
|
||||
text: 'text-orange-600 dark:text-orange-400',
|
||||
accent: 'from-orange-500 to-red-500',
|
||||
},
|
||||
};
|
||||
|
||||
export default function AdminTalentPool() {
|
||||
const { data: profiles = [], isLoading } = useWorkerProfiles();
|
||||
|
||||
@@ -80,23 +124,16 @@ export default function AdminTalentPool() {
|
||||
setSearch(''); setSkill('all'); setExperience('all'); setLocation('all'); setBand('all'); setAvailability('all');
|
||||
};
|
||||
|
||||
/**
|
||||
* Talent segments.
|
||||
*
|
||||
* The pool's shape by career score band, with how many in each band are
|
||||
* actually available. Score without availability is a shortlist you cannot
|
||||
* staff from, so the two belong in the same read.
|
||||
*/
|
||||
const segments = useMemo(() => {
|
||||
const band = (min, max) => profiles.filter((p) => {
|
||||
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: band(90), tone: 'brand' },
|
||||
{ label: 'Excellent', hint: '75–89', members: band(75, 90), tone: 'success' },
|
||||
{ label: 'Solid', hint: '60–74', members: band(60, 75), tone: 'default' },
|
||||
{ label: 'Building', hint: 'under 60', members: band(1, 60), tone: 'warning' },
|
||||
{ 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,
|
||||
@@ -121,64 +158,84 @@ export default function AdminTalentPool() {
|
||||
title="Talent Pool"
|
||||
subtitle="Discover and manage high-potential talent for future hiring."
|
||||
>
|
||||
{/* Segments — the pool's shape, and how much of each band is available. */}
|
||||
{/* Segments — executive metric cards */}
|
||||
<section aria-labelledby="segments" className="space-y-3">
|
||||
<SectionTitle
|
||||
id="segments"
|
||||
title="Talent segments"
|
||||
meta={`${profiles.length} in the pool`}
|
||||
/>
|
||||
{/* Five compact cards, one per band, across the page's content width — the
|
||||
same measure the directory below uses, so the two sections share a left
|
||||
and right edge. A band is read one at a time, so each gets its own small
|
||||
surface with the count directly above the detail that qualifies it. */}
|
||||
<div>
|
||||
<ul className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
|
||||
{segments.map((s) => {
|
||||
const actionable = s.tone === 'risk';
|
||||
const cfg = SEGMENT_CONFIG[s.label] || SEGMENT_CONFIG.Elite;
|
||||
const Icon = cfg.icon;
|
||||
const isSelected = band === cfg.key;
|
||||
const empty = s.count === 0;
|
||||
|
||||
return (
|
||||
<li
|
||||
<div
|
||||
key={s.label}
|
||||
className="rounded-xl border border-border bg-surface px-3.5 py-3 transition-colors duration-base hover:border-ink-4/40"
|
||||
onClick={() => setBand(isSelected ? 'all' : cfg.key)}
|
||||
className={cn(
|
||||
'group relative flex flex-col justify-between cursor-pointer rounded-xl border bg-surface p-4 shadow-xs backdrop-blur-xs transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
|
||||
isSelected ? cfg.activeBorder : cfg.border
|
||||
)}
|
||||
>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="truncate text-body-sm font-semibold text-ink-1">{s.label}</span>
|
||||
<span className="truncate text-caption text-ink-4">{s.hint}</span>
|
||||
{/* The one accent in the card: a 5px dot, warning where the
|
||||
band is the actionable one. */}
|
||||
{!empty && (
|
||||
<div>
|
||||
{/* Header Row: Label, Hint & Icon */}
|
||||
<div className="flex items-center justify-between gap-1.5">
|
||||
<div className="flex items-baseline gap-1.5 min-w-0">
|
||||
<span className="truncate text-[11px] font-bold uppercase tracking-wider text-ink-1">
|
||||
{s.label}
|
||||
</span>
|
||||
<span className="truncate text-[10px] font-medium text-ink-4">
|
||||
{s.hint}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn('flex h-7 w-7 shrink-0 items-center justify-center rounded-lg shadow-xs', cfg.bg)}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main count */}
|
||||
<div className="mt-2.5 flex items-baseline gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto h-1.5 w-1.5 shrink-0 translate-y-[-1px] rounded-full',
|
||||
actionable ? 'bg-warning' : s.tone === 'brand' ? 'bg-krow-blue' : 'bg-ink-4/40'
|
||||
'font-heading text-title-xl font-bold leading-none tabular-nums',
|
||||
empty ? 'text-ink-4' : cfg.text
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{s.count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={cn(
|
||||
'mt-2 font-heading text-title-lg font-bold leading-none tabular-nums',
|
||||
empty ? 'text-ink-4' : actionable ? 'text-warning' : 'text-ink-1'
|
||||
)}
|
||||
>
|
||||
{s.count}
|
||||
</p>
|
||||
{/* Sub pill & Accent bar */}
|
||||
<div className="mt-3.5 space-y-2">
|
||||
<span className="inline-flex items-center rounded-full bg-surface-subtle px-2 py-0.5 text-[10px] font-semibold text-ink-3 border border-border/60">
|
||||
{empty
|
||||
? 'No profiles'
|
||||
: [
|
||||
`${s.available} available`,
|
||||
s.label === 'Unscored'
|
||||
? 'needs verification'
|
||||
: s.avgExperience > 0
|
||||
? `${s.avgExperience}y avg`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
|
||||
<p className="mt-1.5 truncate text-[11px] text-ink-4">
|
||||
{empty ? 'No profiles' : [
|
||||
`${s.available} available`,
|
||||
actionable ? 'needs verification' : s.avgExperience > 0 ? `${s.avgExperience}y avg` : null,
|
||||
].filter(Boolean).join(' · ')}
|
||||
</p>
|
||||
</li>
|
||||
<div className={cn('h-0.5 w-full rounded-full bg-gradient-to-r opacity-40 group-hover:opacity-100 transition-opacity', cfg.accent)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 border-t border-border pt-3 text-caption leading-relaxed text-ink-3">
|
||||
<p className="mt-3 border-t border-border/60 pt-3 text-caption leading-relaxed text-ink-3">
|
||||
{segments[4].count > segments[0].count + segments[1].count
|
||||
? `${segments[4].count} workers carry no career score against ${segments[0].count + segments[1].count} at Excellent or above. Supply is not the constraint here — verification is.`
|
||||
: 'The scored part of the pool outweighs the unscored, so matching has enough to work with.'}
|
||||
|
||||
Reference in New Issue
Block a user