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

@@ -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.'}