import React, { useMemo, useState } from 'react';
import { Mail, Phone, UserCheck } from 'lucide-react';
import { Avatar, Badge, Drawer, StatusBadge, Surface } from '@/components/ds';
import { useApplications, useJobPostings, useStaff } from '@/lib/krowHooks';
import { AdminPage, SectionTitle } from '@/components/admin/PageShell';
import { SkillSurface } from '@/components/skills/SkillSurface';
import { buildHires, summarise } from '@/lib/hiringRecords';
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 { DAY, RANGES, formatDate } from '@/pages/admin/hired-history/nodes';
import '@/components/ui-tree/nodeTypes';
import '@/pages/admin/hired-history/nodes';
import { useTranslation } from 'react-i18next';
/**
* Admin Hired History — the record of who was hired.
*
* This page and Analytics used to be one component rendered twice, which is why
* they read as duplicates. They answer different questions, and this is the one
* about *people*: **who did we hire, for which position, when, and what
* happened since.**
*
* So it is built as a record rather than as a dashboard. There is no funnel, no
* trend line and no departmental comparison here — those are aggregate readings
* and they live on Analytics. What is here instead is what a record needs and a
* dashboard cannot give you: a search across every hire, filters for the fields
* you would search by, a chronology, and a full detail view for one person.
*
* The figures at the top are a count of what the filters currently select, not
* a performance summary — they answer "how much of the record am I looking at".
* Both pages read `lib/hiringRecords.js`, so the totals cannot disagree.
*/
/** One label/value row in the detail panel. */
function Fact({ label, children }) {
return (
{label}
{children ?? '—'}
);
}
/** The full record for one hire — the page's own detail view. */
function HireDetail({ hire, onClose }) {
const { t } = useTranslation();
if (!hire) return null;
return (
{ if (!next) onClose(); }}
title={hire.name}
description={[hire.role, hire.company].filter((v) => v && v !== '—').join(' · ') || 'Hired'}
icon={UserCheck}
size="default"
>
{hire.name}
{hire.profile_tier && {hire.profile_tier}}
{hire.role}
{hire.company}
{hire.department}
{formatDate(hire.hire_date)}
{hire.timeToHire ? `${hire.timeToHire} days` : null}
{hire.score}
{hire.client_rating ? `${hire.client_rating} / 5` : 'Not reviewed yet'}
{(hire.email || hire.phone) && (
)}
{/* Per-record extension point, on the same terms as every other
surface: a definition names `hired-history`, this renders it. */}
);
}
export default function AdminHiredHistory() {
const { t } = useTranslation();
const { data: staff = [], isLoading } = useStaff();
const { data: applications = [] } = useApplications();
const { data: postings = [] } = useJobPostings();
const [search, setSearch] = useState('');
const [filters, setFilters] = useState({ position: 'all', department: 'all', range: 'all' });
const [selected, setSelected] = useState(null);
const hires = useMemo(
() => buildHires({ staff, applications, postings }),
[staff, applications, postings]
);
/* Filter options come from the record itself, so a department with no hires
is never offered as a filter that returns nothing. */
const positionOptions = useMemo(
() => [...new Set(hires.map((h) => h.role).filter((r) => r && r !== '—'))].sort(),
[hires]
);
const departmentOptions = useMemo(
() => [...new Set(hires.map((h) => h.department).filter((d) => d && d !== '—'))].sort(),
[hires]
);
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
const range = RANGES.find((r) => r.value === filters.range);
const cutoff = range?.days ? Date.now() - range.days * DAY : null;
return hires
.filter((h) => {
if (q && ![h.name, h.role, h.company, h.department, h.email]
.some((v) => String(v || '').toLowerCase().includes(q))) return false;
if (filters.position !== 'all' && h.role !== filters.position) return false;
if (filters.department !== 'all' && h.department !== filters.department) return false;
if (cutoff && new Date(h.hire_date || 0).getTime() < cutoff) return false;
return true;
})
.sort((a, b) => new Date(b.hire_date || 0).getTime() - new Date(a.hire_date || 0).getTime());
}, [hires, search, filters]);
const summary = useMemo(() => summarise(filtered), [filtered]);
const isFiltered = Boolean(search) || Object.values(filters).some((v) => v !== 'all');
/* Recent first, and only what a chronology can actually show — a timeline of
forty rows is a table with worse density. */
const recent = useMemo(() => filtered.filter((h) => h.hire_date).slice(0, 6), [filtered]);
const clearFilters = () => {
setSearch('');
setFilters({ position: 'all', department: 'all', range: 'all' });
};
/* What this page's sections read. Published once; the renderer passes it
down untouched and never looks inside. */
const context = useMemo(() => ({
isLoading, hires, filtered, recent, summary, isFiltered,
search, setSearch, filters, setFilters,
positionOptions, departmentOptions, clearFilters, setSelected,
}), [
isLoading, hires, filtered, recent, summary, isFiltered,
search, filters, positionOptions, departmentOptions,
]);
return (
{/* One person, in full. An overlay rather than a section of the page, so
it stays outside the tree — there is nothing to reorder about a
drawer. */}
setSelected(null)} />
);
}
/** The composed page. Split out so it can read the tree the provider computed. */
function HiredComposition({ context }) {
const editing = useUiEditing();
/* Outside a layout — a test, a baseline capture — there is no session, and
the page still has to draw. It falls back to the composition as shipped. */
const tree = editing?.tree || composePage('hired-history').tree;
return ;
}