ui-polish 3/6: screen sweep A (9 representative screens)
Dashboard, Candidates, Jobs, Settings, Inbox, Interviews, Matching, Managers, JobCandidates — bounded checklist per screen: - Hand-rolled .page-head blocks replaced with <PageHeader/>. Fixes Matching, whose bare <h1> rendered in the wrong face and weight. - Candidates drops its verbatim copy of the DataTable thead for DataTableHead; clickable rows use .row-click. - Interviews deletes its local STATUS_CLASS shadow (Badge auto-maps identically); Dashboard imports the shared PRIORITY_CLASS. - Icon-only .act-btn buttons gain aria-labels mirroring their tooltips. - Emails no longer overflow: Settings/Candidates clip with full text in title=""; Managers shows the full address again instead of chopping the domain off the data. - Mechanical inline-style swaps to the new utilities (mb-*, mt-*, flex-1, justify-between, flex-wrap) on exact matches only. - Settings uses its imported EmptyState instead of a hand-rolled copy; JobCandidates embedded title uses .page-title-sm instead of an inline font size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>pull/29/head
parent
bd16f8a775
commit
4757895029
|
|
@ -14,7 +14,8 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
import { Pagination, useDataTable } from '../ui/DataTable'
|
import { DataTableHead, Pagination, useDataTable } from '../ui/DataTable'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Avatar, Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
|
import { Avatar, Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
import CandidateProfile from './CandidateProfile'
|
import CandidateProfile from './CandidateProfile'
|
||||||
|
|
@ -263,14 +264,10 @@ export default function Candidates() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title="Candidates"
|
||||||
<h1 className="page-title">Candidates</h1>
|
sub={<>{rows.length} candidate account{rows.length === 1 ? '' : 's'} · role_id {CANDIDATE_ROLE_ID}</>}
|
||||||
<p className="page-sub">
|
actions={<>
|
||||||
{rows.length} candidate account{rows.length === 1 ? '' : 's'} · role_id {CANDIDATE_ROLE_ID}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="page-head-actions">
|
|
||||||
<button className="btn btn-secondary" onClick={() => toast('Candidates exported', 'success')}>
|
<button className="btn btn-secondary" onClick={() => toast('Candidates exported', 'success')}>
|
||||||
<Icon name="download" /> Export
|
<Icon name="download" /> Export
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -280,11 +277,11 @@ export default function Candidates() {
|
||||||
<button className="btn btn-primary" onClick={() => setAdding(true)}>
|
<button className="btn btn-primary" onClick={() => setAdding(true)}>
|
||||||
<Icon name="plus" /> Add Candidate
|
<Icon name="plus" /> Add Candidate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{recentChips.length > 0 && (
|
{recentChips.length > 0 && (
|
||||||
<div className="flex items-center gap-8" style={{ marginBottom: 14, flexWrap: 'wrap' }}>
|
<div className="flex items-center gap-8 flex-wrap mb-12">
|
||||||
<span className="text-muted text-sm fw-600">Recently viewed:</span>
|
<span className="text-muted text-sm fw-600">Recently viewed:</span>
|
||||||
{recentChips.map((c) => (
|
{recentChips.map((c) => (
|
||||||
<button key={c.id} className="prompt-chip" style={{ padding: '5px 10px' }} onClick={() => openProfile(c)}>
|
<button key={c.id} className="prompt-chip" style={{ padding: '5px 10px' }} onClick={() => openProfile(c)}>
|
||||||
|
|
@ -342,30 +339,7 @@ export default function Candidates() {
|
||||||
<div className="dt">
|
<div className="dt">
|
||||||
<div className="table-wrap">
|
<div className="table-wrap">
|
||||||
<table className="data">
|
<table className="data">
|
||||||
<thead>
|
<DataTableHead columns={columns} sort={t.sort} toggleSort={t.toggleSort} />
|
||||||
<tr>
|
|
||||||
{columns.map((c) => {
|
|
||||||
const isSorted = t.sort.key === c.key
|
|
||||||
const cls = [
|
|
||||||
c.sortable ? 'sortable' : '',
|
|
||||||
isSorted ? (t.sort.dir === 1 ? 'sorted-asc' : 'sorted-desc') : '',
|
|
||||||
].filter(Boolean).join(' ')
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
key={c.key}
|
|
||||||
className={cls}
|
|
||||||
style={{ textAlign: c.align || 'left' }}
|
|
||||||
onClick={c.sortable ? () => t.toggleSort(c.key) : undefined}
|
|
||||||
>
|
|
||||||
{c.label}
|
|
||||||
{c.sortable && (
|
|
||||||
<span className="sort-ind">{isSorted ? (t.sort.dir === 1 ? '▲' : '▼') : '⇅'}</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{t.pageRows.length === 0 ? (
|
{t.pageRows.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -379,7 +353,7 @@ export default function Candidates() {
|
||||||
t.pageRows.map((c) => (
|
t.pageRows.map((c) => (
|
||||||
<tr
|
<tr
|
||||||
key={c.id}
|
key={c.id}
|
||||||
style={{ cursor: 'pointer' }}
|
className="row-click"
|
||||||
onClick={() => openProfile(c)}
|
onClick={() => openProfile(c)}
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -392,7 +366,7 @@ export default function Candidates() {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="text-sm">{c.email ?? '—'}</span>
|
<span className="text-sm cell-clip" title={c.email || undefined}>{c.email ?? '—'}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{c.isActive
|
{c.isActive
|
||||||
|
|
@ -948,7 +922,7 @@ function AddCandidate({ onClose, onSave, onInvalid }) {
|
||||||
<div className="dz-icn" style={{ width: 44, height: 44, borderRadius: 13, marginBottom: 10 }}>
|
<div className="dz-icn" style={{ width: 44, height: 44, borderRadius: 13, marginBottom: 10 }}>
|
||||||
<Icon name="upload" />
|
<Icon name="upload" />
|
||||||
</div>
|
</div>
|
||||||
<h3 style={{ fontSize: 15 }}>Drop the CV here or click to browse</h3>
|
<h3>Drop the CV here or click to browse</h3>
|
||||||
<p className="text-muted text-sm">
|
<p className="text-muted text-sm">
|
||||||
PDF only · text-based resumes · up to {MAX_CV_MB} MB
|
PDF only · text-based resumes · up to {MAX_CV_MB} MB
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -956,7 +930,7 @@ function AddCandidate({ onClose, onSave, onInvalid }) {
|
||||||
{cv && (
|
{cv && (
|
||||||
<div className="upload-row">
|
<div className="upload-row">
|
||||||
<span className="attach-icn" style={{ width: 34, height: 34 }}><Icon name="file" /></span>
|
<span className="attach-icn" style={{ width: 34, height: 34 }}><Icon name="file" /></span>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1">
|
||||||
<div className="fw-600 text-sm">{cv.name}</div>
|
<div className="fw-600 text-sm">{cv.name}</div>
|
||||||
<div className="cell-sub">{Math.max(1, Math.round(cv.size / 1024))} KB</div>
|
<div className="cell-sub">{Math.max(1, Math.round(cv.size / 1024))} KB</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import Chart, { ChartLegend } from '../ui/Chart'
|
import Chart, { ChartLegend } from '../ui/Chart'
|
||||||
import Charts from '../lib/charts'
|
import Charts from '../lib/charts'
|
||||||
import Dropdown from '../ui/Dropdown'
|
import Dropdown from '../ui/Dropdown'
|
||||||
import { Avatar, Badge, EmptyState, Icon, KpiTile, ProgressBar } from '../ui/primitives'
|
import PageHeader from '../ui/PageHeader'
|
||||||
|
import { Avatar, Badge, EmptyState, Icon, KpiTile, PRIORITY_CLASS, ProgressBar } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
import { useAuth } from '../auth/AuthContext'
|
import { useAuth } from '../auth/AuthContext'
|
||||||
import { qk } from '../lib/queryKeys'
|
import { qk } from '../lib/queryKeys'
|
||||||
|
|
@ -17,7 +18,6 @@ import * as jobsApi from '../api/jobs'
|
||||||
import * as tasksApi from '../api/tasks'
|
import * as tasksApi from '../api/tasks'
|
||||||
|
|
||||||
const POLL_MS = 60_000
|
const POLL_MS = 60_000
|
||||||
const PRIORITY_CLASS = { High: 'b-red', Medium: 'b-amber', Low: 'b-gray' }
|
|
||||||
|
|
||||||
function asObject(data) {
|
function asObject(data) {
|
||||||
return data && typeof data === 'object' && !Array.isArray(data) ? data : null
|
return data && typeof data === 'object' && !Array.isArray(data) ? data : null
|
||||||
|
|
@ -373,14 +373,10 @@ export default function Dashboard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title={<>{greetingFor()}, {firstName} 👋</>}
|
||||||
<h1 className="page-title">{greetingFor()}, {firstName} 👋</h1>
|
sub={<>Here’s what’s happening with your hiring today — {todayLabel}</>}
|
||||||
<p className="page-sub">
|
actions={<>
|
||||||
Here’s what’s happening with your hiring today — {todayLabel}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="page-head-actions">
|
|
||||||
<Link className="btn btn-secondary" to="/reports">
|
<Link className="btn btn-secondary" to="/reports">
|
||||||
<Icon name="download" /> Export
|
<Icon name="download" /> Export
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -404,8 +400,8 @@ export default function Dashboard() {
|
||||||
<Link className="btn btn-primary" to="/jobs" state={{ openCreate: true }}>
|
<Link className="btn btn-primary" to="/jobs" state={{ openCreate: true }}>
|
||||||
<Icon name="plus" /> Create Job
|
<Icon name="plus" /> Create Job
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</>}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="grid g-kpi-7">
|
<div className="grid g-kpi-7">
|
||||||
{tiles.map((c) => <KpiTile key={c.label} {...c} />)}
|
{tiles.map((c) => <KpiTile key={c.label} {...c} />)}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
import EmailBody, { looksLikeHtml } from '../ui/EmailBody'
|
import EmailBody, { looksLikeHtml } from '../ui/EmailBody'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Tabs } from '../ui/Tabs'
|
import { Tabs } from '../ui/Tabs'
|
||||||
import { Pagination, pageWindow } from '../ui/DataTable'
|
import { Pagination, pageWindow } from '../ui/DataTable'
|
||||||
import { Avatar, Badge, EmptyState, Icon, ScoreChip } from '../ui/primitives'
|
import { Avatar, Badge, EmptyState, Icon, ScoreChip } from '../ui/primitives'
|
||||||
|
|
@ -719,12 +720,10 @@ export default function Inbox() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title="Recruitment Inbox"
|
||||||
<h1 className="page-title">Recruitment Inbox</h1>
|
sub="Every candidate, every source — one unified queue"
|
||||||
<p className="page-sub">Every candidate, every source — one unified queue</p>
|
actions={<>
|
||||||
</div>
|
|
||||||
<div className="page-head-actions">
|
|
||||||
<span className="integration-status"><span className="pulse" />Microsoft Graph API · Connected</span>
|
<span className="integration-status"><span className="pulse" />Microsoft Graph API · Connected</span>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
|
|
@ -739,8 +738,8 @@ export default function Inbox() {
|
||||||
<button className="btn btn-primary" onClick={() => navigate('/import')}>
|
<button className="btn btn-primary" onClick={() => navigate('/import')}>
|
||||||
<Icon name="upload" /> Upload CVs
|
<Icon name="upload" /> Upload CVs
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div style={{ margin: '0 16px', paddingTop: 8 }}>
|
<div style={{ margin: '0 16px', paddingTop: 8 }}>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import DataTable from '../ui/DataTable'
|
import DataTable from '../ui/DataTable'
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Tabs } from '../ui/Tabs'
|
import { Tabs } from '../ui/Tabs'
|
||||||
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, Stars } from '../ui/primitives'
|
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, Stars } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
|
|
@ -39,13 +40,6 @@ import { avatarColor, fmtShort, initials as initialsOf } from '../data/seed'
|
||||||
|
|
||||||
const FETCH_TOP = 200
|
const FETCH_TOP = 200
|
||||||
|
|
||||||
const STATUS_CLASS = {
|
|
||||||
Scheduled: 'b-blue',
|
|
||||||
Completed: 'b-green',
|
|
||||||
Cancelled: 'b-red',
|
|
||||||
'No Show': 'b-amber',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** <input type="date"> + <input type="time"> -> one ISO instant, or null. */
|
/** <input type="date"> + <input type="time"> -> one ISO instant, or null. */
|
||||||
function toInstant(date, time) {
|
function toInstant(date, time) {
|
||||||
if (!date) return null
|
if (!date) return null
|
||||||
|
|
@ -229,14 +223,14 @@ export default function Interviews() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status', label: 'Status', sortable: true,
|
key: 'status', label: 'Status', sortable: true,
|
||||||
render: (iv) => <Badge className={STATUS_CLASS[iv.status] ?? 'b-gray'}>{iv.status}</Badge>,
|
render: (iv) => <Badge>{iv.status}</Badge>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '_a', label: 'Actions', align: 'right',
|
key: '_a', label: 'Actions', align: 'right',
|
||||||
render: (iv) => (
|
render: (iv) => (
|
||||||
<div className="row-actions">
|
<div className="row-actions">
|
||||||
<button
|
<button
|
||||||
className="act-btn" data-tip="View candidate"
|
className="act-btn" data-tip="View candidate" aria-label="View candidate"
|
||||||
disabled={!iv.userId}
|
disabled={!iv.userId}
|
||||||
onClick={() => navigate('/candidates', { state: { openCandidate: iv.userId } })}
|
onClick={() => navigate('/candidates', { state: { openCandidate: iv.userId } })}
|
||||||
>
|
>
|
||||||
|
|
@ -245,14 +239,14 @@ export default function Interviews() {
|
||||||
{iv.status === 'Scheduled' && (
|
{iv.status === 'Scheduled' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="act-btn" data-tip="Mark completed"
|
className="act-btn" data-tip="Mark completed" aria-label="Mark completed"
|
||||||
disabled={setStatusMutation.isPending}
|
disabled={setStatusMutation.isPending}
|
||||||
onClick={() => setStatusMutation.mutate({ id: iv.id, next: 'Completed' })}
|
onClick={() => setStatusMutation.mutate({ id: iv.id, next: 'Completed' })}
|
||||||
>
|
>
|
||||||
<Icon name="check" />
|
<Icon name="check" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="act-btn" data-tip="Cancel interview"
|
className="act-btn" data-tip="Cancel interview" aria-label="Cancel interview"
|
||||||
disabled={setStatusMutation.isPending}
|
disabled={setStatusMutation.isPending}
|
||||||
onClick={() => setStatusMutation.mutate({ id: iv.id, next: 'Cancelled' })}
|
onClick={() => setStatusMutation.mutate({ id: iv.id, next: 'Cancelled' })}
|
||||||
>
|
>
|
||||||
|
|
@ -260,7 +254,7 @@ export default function Interviews() {
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button className="act-btn" data-tip="Scorecard" onClick={() => setFeedbackFor(iv)}>
|
<button className="act-btn" data-tip="Scorecard" aria-label="Scorecard" onClick={() => setFeedbackFor(iv)}>
|
||||||
<Icon name="star" />
|
<Icon name="star" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -272,18 +266,16 @@ export default function Interviews() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title="Interviews"
|
||||||
<h1 className="page-title">Interviews</h1>
|
sub="Manage and track all interview activity"
|
||||||
<p className="page-sub">Manage and track all interview activity</p>
|
actions={<>
|
||||||
</div>
|
|
||||||
<div className="page-head-actions">
|
|
||||||
<Link className="btn btn-secondary" to="/calendar"><Icon name="calendar" /> Calendar View</Link>
|
<Link className="btn btn-secondary" to="/calendar"><Icon name="calendar" /> Calendar View</Link>
|
||||||
<button className="btn btn-primary" onClick={() => setScheduling(true)}>
|
<button className="btn btn-primary" onClick={() => setScheduling(true)}>
|
||||||
<Icon name="plus" /> Schedule Interview
|
<Icon name="plus" /> Schedule Interview
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="grid g-kpi mb-18">
|
<div className="grid g-kpi mb-18">
|
||||||
<KpiCard label="Scheduled" value={allQuery.isPending ? '—' : stats.scheduled} icon="calendar" tone="i-blue" />
|
<KpiCard label="Scheduled" value={allQuery.isPending ? '—' : stats.scheduled} icon="calendar" tone="i-blue" />
|
||||||
|
|
@ -484,14 +476,14 @@ function Scorecard({ interview: iv, onClose, onSaved, toast }) {
|
||||||
color={avatarColor(iv.candidate)}
|
color={avatarColor(iv.candidate)}
|
||||||
className="avatar-lg"
|
className="avatar-lg"
|
||||||
/>
|
/>
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div className="ph-name" style={{ fontSize: 17 }}>{iv.candidate}</div>
|
<div className="ph-name" style={{ fontSize: 17 }}>{iv.candidate}</div>
|
||||||
<div className="ph-role">{iv.type}{iv.jobTitle ? ` · ${iv.jobTitle}` : ''}</div>
|
<div className="ph-role">{iv.type}{iv.jobTitle ? ` · ${iv.jobTitle}` : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={STATUS_CLASS[iv.status] ?? 'b-gray'}>{iv.status}</Badge>
|
<Badge>{iv.status}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 18 }}>
|
<div className="mb-18">
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tab}
|
value={tab}
|
||||||
onChange={setTab}
|
onChange={setTab}
|
||||||
|
|
@ -519,14 +511,14 @@ function Scorecard({ interview: iv, onClose, onSaved, toast }) {
|
||||||
)}
|
)}
|
||||||
{template && (
|
{template && (
|
||||||
<>
|
<>
|
||||||
<div className="form-field" style={{ marginBottom: 8 }}>
|
<div className="form-field mb-8">
|
||||||
<label>Evaluation Template</label>
|
<label>Evaluation Template</label>
|
||||||
<select value={templateName} onChange={(e) => setTemplateName(e.target.value)}>
|
<select value={templateName} onChange={(e) => setTemplateName(e.target.value)}>
|
||||||
{templates.map((t) => <option key={t.id}>{t.name}</option>)}
|
{templates.map((t) => <option key={t.id}>{t.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<CriteriaList criteria={template.criteria} ratings={ratings} setRating={setRating} />
|
<CriteriaList criteria={template.criteria} ratings={ratings} setRating={setRating} />
|
||||||
<div className="form-field" style={{ marginTop: 8 }}>
|
<div className="form-field mt-8">
|
||||||
<label>Comments</label>
|
<label>Comments</label>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Strengths, concerns, and areas explored…"
|
placeholder="Strengths, concerns, and areas explored…"
|
||||||
|
|
@ -534,7 +526,7 @@ function Scorecard({ interview: iv, onClose, onSaved, toast }) {
|
||||||
onChange={(e) => setComments(e.target.value)}
|
onChange={(e) => setComments(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-field" style={{ marginTop: 14 }}>
|
<div className="form-field mt-12">
|
||||||
<label>Overall Recommendation</label>
|
<label>Overall Recommendation</label>
|
||||||
<div className="seg" style={{ marginTop: 4 }}>
|
<div className="seg" style={{ marginTop: 4 }}>
|
||||||
{['Strong Hire', 'Hire', 'Lean Hire', 'No Hire'].map((r) => (
|
{['Strong Hire', 'Hire', 'Lean Hire', 'No Hire'].map((r) => (
|
||||||
|
|
@ -662,7 +654,7 @@ function ScheduleForm({ applications, loading, busy, onClose, onSubmit, toast })
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted" style={{ marginTop: 16, fontSize: 13 }}>
|
<p className="text-muted text-sm mt-16">
|
||||||
Interviews attach to an application, so only candidates with an assigned job post appear here.
|
Interviews attach to an application, so only candidates with an assigned job post appear here.
|
||||||
Duration, meeting mode and interviewers are not stored by the interview record.
|
Duration, meeting mode and interviewers are not stored by the interview record.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ function CandidateCard({ c, onView }) {
|
||||||
<span className="cand-meta"><Icon name="briefcase" /> {c.experience != null ? `${c.experience} yrs` : '—'}</span>
|
<span className="cand-meta"><Icon name="briefcase" /> {c.experience != null ? `${c.experience} yrs` : '—'}</span>
|
||||||
<span className="cand-company">{c.currentCompany ?? ''}</span>
|
<span className="cand-company">{c.currentCompany ?? ''}</span>
|
||||||
<Badge className="b-gray">{SOURCE_LABEL[c.source] ?? c.source}</Badge>
|
<Badge className="b-gray">{SOURCE_LABEL[c.source] ?? c.source}</Badge>
|
||||||
<button className="act-btn" data-tip="View" onClick={(e) => { e.stopPropagation(); onView(c) }}>
|
<button className="act-btn" data-tip="View" aria-label="View candidate" onClick={(e) => { e.stopPropagation(); onView(c) }}>
|
||||||
<Icon name="eye" />
|
<Icon name="eye" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,7 +119,7 @@ function FailedCard({ c, onView }) {
|
||||||
<Badge className="b-red">{c.errorCode ?? 'FAILED'}</Badge>
|
<Badge className="b-red">{c.errorCode ?? 'FAILED'}</Badge>
|
||||||
<span className="cand-company" />
|
<span className="cand-company" />
|
||||||
<Badge className="b-gray">{SOURCE_LABEL[c.source] ?? c.source}</Badge>
|
<Badge className="b-gray">{SOURCE_LABEL[c.source] ?? c.source}</Badge>
|
||||||
<button className="act-btn" data-tip="View" onClick={(e) => { e.stopPropagation(); onView(c) }}>
|
<button className="act-btn" data-tip="View" aria-label="View details" onClick={(e) => { e.stopPropagation(); onView(c) }}>
|
||||||
<Icon name="eye" />
|
<Icon name="eye" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -143,7 +143,7 @@ export function ScoredCandidateDetail({ candidate: c, jobTitle, onClose }) {
|
||||||
>
|
>
|
||||||
<div className="profile-hero">
|
<div className="profile-hero">
|
||||||
<Avatar name={c.name} initials={initialsOf(c.name)} color={avatarColor(c.name)} className="avatar-lg" />
|
<Avatar name={c.name} initials={initialsOf(c.name)} color={avatarColor(c.name)} className="avatar-lg" />
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div className="ph-name">{displayName(c.name)}</div>
|
<div className="ph-name">{displayName(c.name)}</div>
|
||||||
<div className="ph-role">{roleLine}</div>
|
<div className="ph-role">{roleLine}</div>
|
||||||
<div className="ph-tags">
|
<div className="ph-tags">
|
||||||
|
|
@ -195,14 +195,14 @@ export function ScoredCandidateDetail({ candidate: c, jobTitle, onClose }) {
|
||||||
{tab === 'Job Match' && completed && (
|
{tab === 'Job Match' && completed && (
|
||||||
<>
|
<>
|
||||||
<div className="form-section-title" style={{ marginTop: 0 }}>Job-Match Score</div>
|
<div className="form-section-title" style={{ marginTop: 0 }}>Job-Match Score</div>
|
||||||
<p className="text-muted text-sm" style={{ marginBottom: 14 }}>
|
<p className="text-muted text-sm mb-12">
|
||||||
Match this candidate against the job the CV was scored for.
|
Match this candidate against the job the CV was scored for.
|
||||||
</p>
|
</p>
|
||||||
<div className="card" style={{ boxShadow: 'none', background: 'var(--bg-sunken)', marginBottom: 20 }}>
|
<div className="card" style={{ boxShadow: 'none', background: 'var(--bg-sunken)', marginBottom: 20 }}>
|
||||||
<div className="card-body flex items-center gap-12">
|
<div className="card-body flex items-center gap-12">
|
||||||
<MiniRing score={c.aiScore} size={56} />
|
<MiniRing score={c.aiScore} size={56} />
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1">
|
||||||
<div className="fw-600" style={{ marginBottom: 8 }}>{jobTitle ?? 'Selected job'}</div>
|
<div className="fw-600 mb-8">{jobTitle ?? 'Selected job'}</div>
|
||||||
<ProgressBar pct={c.aiScore} />
|
<ProgressBar pct={c.aiScore} />
|
||||||
</div>
|
</div>
|
||||||
{band && <Badge className={bandCls}>{band}</Badge>}
|
{band && <Badge className={bandCls}>{band}</Badge>}
|
||||||
|
|
@ -212,7 +212,7 @@ export function ScoredCandidateDetail({ candidate: c, jobTitle, onClose }) {
|
||||||
<div className="form-section-title" style={{ marginTop: 0 }}>
|
<div className="form-section-title" style={{ marginTop: 0 }}>
|
||||||
Matched must-have skills ({c.matchedSkills.length})
|
Matched must-have skills ({c.matchedSkills.length})
|
||||||
</div>
|
</div>
|
||||||
<div className="k-tags" style={{ marginBottom: 16 }}>
|
<div className="k-tags mb-16">
|
||||||
{c.matchedSkills.length
|
{c.matchedSkills.length
|
||||||
? c.matchedSkills.map((s) => (
|
? c.matchedSkills.map((s) => (
|
||||||
<span className="skill-pill skill-matched" key={s}><Icon name="check" /> {s}</span>
|
<span className="skill-pill skill-matched" key={s}><Icon name="check" /> {s}</span>
|
||||||
|
|
@ -223,7 +223,7 @@ export function ScoredCandidateDetail({ candidate: c, jobTitle, onClose }) {
|
||||||
<div className="form-section-title" style={{ marginTop: 0 }}>
|
<div className="form-section-title" style={{ marginTop: 0 }}>
|
||||||
Missing must-have skills ({c.missingSkills.length})
|
Missing must-have skills ({c.missingSkills.length})
|
||||||
</div>
|
</div>
|
||||||
<div className="k-tags" style={{ marginBottom: 16 }}>
|
<div className="k-tags mb-16">
|
||||||
{c.missingSkills.length
|
{c.missingSkills.length
|
||||||
? c.missingSkills.map((s) => (
|
? c.missingSkills.map((s) => (
|
||||||
<span className="skill-pill skill-missing" key={s}><Icon name="x" /> {s}</span>
|
<span className="skill-pill skill-missing" key={s}><Icon name="x" /> {s}</span>
|
||||||
|
|
@ -282,9 +282,9 @@ export default function JobCandidates({ jobId, jobTitle }) {
|
||||||
if (!jobId) return null
|
if (!jobId) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 24 }}>
|
<div className="mt-24">
|
||||||
<div style={{ marginBottom: 14 }}>
|
<div className="mb-12">
|
||||||
<h2 className="page-title" style={{ fontSize: 20 }}>Candidates</h2>
|
<h2 className="page-title page-title-sm">Candidates</h2>
|
||||||
<p className="page-sub">
|
<p className="page-sub">
|
||||||
{rows.length} candidate{rows.length === 1 ? '' : 's'} · {scored} scored · {failed} failed
|
{rows.length} candidate{rows.length === 1 ? '' : 's'} · {scored} scored · {failed} failed
|
||||||
{jobTitle ? ` · vs ${jobTitle}` : ''}
|
{jobTitle ? ` · vs ${jobTitle}` : ''}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import AiFieldAssist from '../ui/AiFieldAssist'
|
import AiFieldAssist from '../ui/AiFieldAssist'
|
||||||
import DataTable from '../ui/DataTable'
|
import DataTable from '../ui/DataTable'
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
|
import { Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
import { useAuth } from '../auth/AuthContext'
|
import { useAuth } from '../auth/AuthContext'
|
||||||
|
|
@ -221,11 +222,11 @@ export default function Jobs() {
|
||||||
key: '_a', label: 'Actions', align: 'right',
|
key: '_a', label: 'Actions', align: 'right',
|
||||||
render: (j) => (
|
render: (j) => (
|
||||||
<div className="row-actions">
|
<div className="row-actions">
|
||||||
<button className="act-btn" data-tip="View" onClick={() => setViewing(j)}><Icon name="eye" /></button>
|
<button className="act-btn" data-tip="View" aria-label="View job" onClick={() => setViewing(j)}><Icon name="eye" /></button>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<button className="act-btn" data-tip="Edit" onClick={() => setEditing(j)}><Icon name="edit" /></button>
|
<button className="act-btn" data-tip="Edit" aria-label="Edit job" onClick={() => setEditing(j)}><Icon name="edit" /></button>
|
||||||
)}
|
)}
|
||||||
<button className="act-btn" data-tip="Publish" onClick={() => navigate('/jobboard', { state: { publishJob: j.id } })}><Icon name="send" /></button>
|
<button className="act-btn" data-tip="Publish" aria-label="Publish job" onClick={() => navigate('/jobboard', { state: { publishJob: j.id } })}><Icon name="send" /></button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -233,12 +234,10 @@ export default function Jobs() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title="Jobs"
|
||||||
<h1 className="page-title">Jobs</h1>
|
sub={`${jobs.length} requisitions · ${openCount} currently open`}
|
||||||
<p className="page-sub">{jobs.length} requisitions · {openCount} currently open</p>
|
actions={<>
|
||||||
</div>
|
|
||||||
<div className="page-head-actions">
|
|
||||||
<button className="btn btn-secondary" onClick={exportJobs} disabled={exporting}>
|
<button className="btn btn-secondary" onClick={exportJobs} disabled={exporting}>
|
||||||
<Icon name="download" /> {exporting ? 'Exporting…' : 'Export'}
|
<Icon name="download" /> {exporting ? 'Exporting…' : 'Export'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -247,8 +246,8 @@ export default function Jobs() {
|
||||||
<Icon name="plus" /> Create Job <Icon name="sparkles" />
|
<Icon name="plus" /> Create Job <Icon name="sparkles" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
{jobsQuery.isPending && (
|
{jobsQuery.isPending && (
|
||||||
|
|
@ -620,7 +619,7 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) {
|
||||||
) : (
|
) : (
|
||||||
<span className="attach-icn" style={{ width: 34, height: 34 }}><Icon name="file" /></span>
|
<span className="attach-icn" style={{ width: 34, height: 34 }}><Icon name="file" /></span>
|
||||||
)}
|
)}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1">
|
||||||
<div className="fw-600 text-sm">{imageFile.name}</div>
|
<div className="fw-600 text-sm">{imageFile.name}</div>
|
||||||
<div className="cell-sub">{Math.max(1, Math.round(imageFile.size / 1024))} KB</div>
|
<div className="cell-sub">{Math.max(1, Math.round(imageFile.size / 1024))} KB</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -640,7 +639,7 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted" style={{ marginTop: 16, fontSize: 13 }}>
|
<p className="text-muted text-sm mt-16">
|
||||||
Saves the requisition to the board — publish to a channel later from the Job Board.
|
Saves the requisition to the board — publish to a channel later from the Job Board.
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -849,7 +848,7 @@ function RecruiterAssignment({ jobPostId, fallbackName, canEdit }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div className="mb-16">
|
||||||
<div style={SECTION_LABEL}>Recruiter ownership</div>
|
<div style={SECTION_LABEL}>Recruiter ownership</div>
|
||||||
{currentQuery.isError ? (
|
{currentQuery.isError ? (
|
||||||
<p className="text-muted text-sm">
|
<p className="text-muted text-sm">
|
||||||
|
|
@ -967,7 +966,7 @@ function JobDetail({
|
||||||
{j.description && (
|
{j.description && (
|
||||||
<>
|
<>
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div className="mb-16">
|
||||||
<div style={SECTION_LABEL}>Description</div>
|
<div style={SECTION_LABEL}>Description</div>
|
||||||
<p style={{ color: 'var(--text-2)' }}>{j.description}</p>
|
<p style={{ color: 'var(--text-2)' }}>{j.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Avatar, Badge, EmptyState, Icon } from '../ui/primitives'
|
import { Avatar, Badge, EmptyState, Icon } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
import { useAuth } from '../auth/AuthContext'
|
import { useAuth } from '../auth/AuthContext'
|
||||||
|
|
@ -46,12 +47,10 @@ export default function Managers() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title="Hiring Managers"
|
||||||
<h1 className="page-title">Hiring Managers</h1>
|
sub={`${managers.length} managers · ${totalReqs} active requisitions`}
|
||||||
<p className="page-sub">{managers.length} managers · {totalReqs} active requisitions</p>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{managersQuery.isPending && (
|
{managersQuery.isPending && (
|
||||||
<EmptyState icon="managers" title="Loading…">Fetching hiring managers.</EmptyState>
|
<EmptyState icon="managers" title="Loading…">Fetching hiring managers.</EmptyState>
|
||||||
|
|
@ -71,9 +70,9 @@ export default function Managers() {
|
||||||
{managers.map((m) => (
|
{managers.map((m) => (
|
||||||
<div className="card" key={m.id}>
|
<div className="card" key={m.id}>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex items-center gap-12" style={{ marginBottom: 14 }}>
|
<div className="flex items-center gap-12 mb-12">
|
||||||
<Avatar name={m.name} className="avatar-lg" />
|
<Avatar name={m.name} className="avatar-lg" />
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div className="lr-title">{m.name}</div>
|
<div className="lr-title">{m.name}</div>
|
||||||
<div className="lr-sub">{m.title || m.roleName || 'Hiring manager'}</div>
|
<div className="lr-sub">{m.title || m.roleName || 'Hiring manager'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,8 +82,10 @@ export default function Managers() {
|
||||||
<div className="stat-mini"><span className="stat-mini-val">{m.teamSize ?? '—'}</span><span className="stat-mini-lbl">Team Size</span></div>
|
<div className="stat-mini"><span className="stat-mini-val">{m.teamSize ?? '—'}</span><span className="stat-mini-lbl">Team Size</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="divider" style={{ margin: '12px 0' }} />
|
<div className="divider" style={{ margin: '12px 0' }} />
|
||||||
<div className="flex items-center" style={{ justifyContent: 'space-between' }}>
|
<div className="flex items-center justify-between gap-8">
|
||||||
<span className="cell-sub"><Icon name="mail" /> {m.email ? m.email.split('@')[0] : '—'}</span>
|
<span className="cell-sub truncate min-w-0" title={m.email || undefined}>
|
||||||
|
<Icon name="mail" /> {m.email || '—'}
|
||||||
|
</span>
|
||||||
<button className="btn btn-ghost btn-sm" onClick={() => setDetail(m)}>View</button>
|
<button className="btn btn-ghost btn-sm" onClick={() => setDetail(m)}>View</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { useSearchParams } from 'react-router-dom'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import EmailBody, { looksLikeHtml } from '../ui/EmailBody'
|
import EmailBody, { looksLikeHtml } from '../ui/EmailBody'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Tabs } from '../ui/Tabs'
|
import { Tabs } from '../ui/Tabs'
|
||||||
import { Avatar, Badge, EmptyState, Icon } from '../ui/primitives'
|
import { Avatar, Badge, EmptyState, Icon } from '../ui/primitives'
|
||||||
import { JobCard, PickRoleModal } from '../ui/SuggestedRoles'
|
import { JobCard, PickRoleModal } from '../ui/SuggestedRoles'
|
||||||
|
|
@ -375,15 +376,10 @@ export default function Matching() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader title="Job Matching" sub="Route applications to the right open role" />
|
||||||
<div>
|
|
||||||
<h1>Job Matching</h1>
|
|
||||||
<p className="page-sub">Route applications to the right open role</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!canEdit && (
|
{!canEdit && (
|
||||||
<div className="alert alert-danger" style={{ marginBottom: 14 }}>
|
<div className="alert alert-danger mb-12">
|
||||||
Your account does not hold <code>inbox.edit</code>, which the server requires to
|
Your account does not hold <code>inbox.edit</code>, which the server requires to
|
||||||
assign, unassign, or retry a match. Controls below stay disabled.
|
assign, unassign, or retry a match. Controls below stay disabled.
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -554,7 +550,7 @@ function MatchingWorkspace({
|
||||||
<div style={{ padding: 24 }}>
|
<div style={{ padding: 24 }}>
|
||||||
<div className="flex items-center gap-16" style={{ marginBottom: 20 }}>
|
<div className="flex items-center gap-16" style={{ marginBottom: 20 }}>
|
||||||
<Avatar name={i.name} initials={i.initials} color={i.color} className="avatar-lg" />
|
<Avatar name={i.name} initials={i.initials} color={i.color} className="avatar-lg" />
|
||||||
<div style={{ flex: 1 }}>
|
<div className="flex-1">
|
||||||
<div className="ph-name" style={{ fontSize: 19 }}>{i.name}</div>
|
<div className="ph-name" style={{ fontSize: 19 }}>{i.name}</div>
|
||||||
<div className="ph-role">{i.position}</div>
|
<div className="ph-role">{i.position}</div>
|
||||||
<div className="ph-tags" style={{ marginTop: 8 }}>
|
<div className="ph-tags" style={{ marginTop: 8 }}>
|
||||||
|
|
@ -577,7 +573,7 @@ function MatchingWorkspace({
|
||||||
marginBottom: 18,
|
marginBottom: 18,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="card-body flex items-center gap-12" style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
<div className="card-body flex items-center gap-12 justify-between flex-wrap">
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<Icon name="check-circle" />
|
<Icon name="check-circle" />
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -609,8 +605,8 @@ function MatchingWorkspace({
|
||||||
>
|
>
|
||||||
<div style={{ flex: '1 1 320px', minWidth: 0 }}>
|
<div style={{ flex: '1 1 320px', minWidth: 0 }}>
|
||||||
{matchFailed ? (
|
{matchFailed ? (
|
||||||
<div className="alert alert-danger" style={{ marginBottom: 16 }}>
|
<div className="alert alert-danger mb-16">
|
||||||
<div style={{ marginBottom: 8 }}>{detail?.matchError || 'Matching failed for this application.'}</div>
|
<div className="mb-8">{detail?.matchError || 'Matching failed for this application.'}</div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary btn-sm"
|
className="btn btn-secondary btn-sm"
|
||||||
disabled={!canEdit || rematching}
|
disabled={!canEdit || rematching}
|
||||||
|
|
@ -621,7 +617,7 @@ function MatchingWorkspace({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div className="mb-16">
|
||||||
<div className="fw-600" style={{ marginBottom: 6 }}>AI verdict</div>
|
<div className="fw-600" style={{ marginBottom: 6 }}>AI verdict</div>
|
||||||
<p style={{ marginBottom: 4 }}>
|
<p style={{ marginBottom: 4 }}>
|
||||||
{detail?.matchSummary || listRow?.matchSummary || 'No match summary yet.'}
|
{detail?.matchSummary || listRow?.matchSummary || 'No match summary yet.'}
|
||||||
|
|
@ -639,7 +635,7 @@ function MatchingWorkspace({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{whyOpen && (
|
{whyOpen && (
|
||||||
<p className="text-muted text-sm" style={{ marginTop: 8 }}>
|
<p className="text-muted text-sm mt-8">
|
||||||
{detail?.matchReasoning || listRow?.matchReasoning}
|
{detail?.matchReasoning || listRow?.matchReasoning}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -649,7 +645,7 @@ function MatchingWorkspace({
|
||||||
{/* Email first: it is the application itself, and the resume is its
|
{/* Email first: it is the application itself, and the resume is its
|
||||||
attachment. Reading order follows that. */}
|
attachment. Reading order follows that. */}
|
||||||
{(detail?.subject || detail?.body) && (
|
{(detail?.subject || detail?.body) && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div className="mb-16">
|
||||||
<div className="fw-600" style={{ marginBottom: 6 }}>Email</div>
|
<div className="fw-600" style={{ marginBottom: 6 }}>Email</div>
|
||||||
<div className="email-head">Subject: {detail.subject || '(no subject)'}</div>
|
<div className="email-head">Subject: {detail.subject || '(no subject)'}</div>
|
||||||
{looksLikeHtml(detail.bodyHtml) ? (
|
{looksLikeHtml(detail.bodyHtml) ? (
|
||||||
|
|
@ -669,7 +665,7 @@ function MatchingWorkspace({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="radiogroup" aria-label="Suggested roles" style={{ flex: '0 1 360px', minWidth: 260 }}>
|
<div role="radiogroup" aria-label="Suggested roles" style={{ flex: '0 1 360px', minWidth: 260 }}>
|
||||||
<div className="fw-600" style={{ marginBottom: 8 }}>Suggested roles</div>
|
<div className="fw-600 mb-8">Suggested roles</div>
|
||||||
{suggestionCards.length === 0 && !manualPost ? (
|
{suggestionCards.length === 0 && !manualPost ? (
|
||||||
<EmptyState icon="alert" title="No suggested roles">
|
<EmptyState icon="alert" title="No suggested roles">
|
||||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap', marginTop: 10 }}>
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap', marginTop: 10 }}>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
|
import PageHeader from '../ui/PageHeader'
|
||||||
import { Tabs } from '../ui/Tabs'
|
import { Tabs } from '../ui/Tabs'
|
||||||
import { Avatar, Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
|
import { Avatar, Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
|
|
@ -115,23 +116,19 @@ export default function Settings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-head">
|
<PageHeader
|
||||||
<div>
|
title="Settings"
|
||||||
<h1 className="page-title">Settings</h1>
|
sub="Configure your workspace and team preferences"
|
||||||
<p className="page-sub">Configure your workspace and team preferences</p>
|
actions={showOrgSave && (
|
||||||
</div>
|
<button
|
||||||
<div className="page-head-actions">
|
className="btn btn-primary"
|
||||||
{showOrgSave && (
|
disabled={!canConfigure || save.isPending}
|
||||||
<button
|
onClick={() => save.mutate()}
|
||||||
className="btn btn-primary"
|
>
|
||||||
disabled={!canConfigure || save.isPending}
|
<Icon name="check" /> {save.isPending ? 'Saving…' : 'Save Changes'}
|
||||||
onClick={() => save.mutate()}
|
</button>
|
||||||
>
|
)}
|
||||||
<Icon name="check" /> {save.isPending ? 'Saving…' : 'Save Changes'}
|
/>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs value={tab} onChange={setTab} tabs={TABS.map((t) => ({ key: t, label: t }))} />
|
<Tabs value={tab} onChange={setTab} tabs={TABS.map((t) => ({ key: t, label: t }))} />
|
||||||
|
|
||||||
|
|
@ -264,14 +261,10 @@ function Users() {
|
||||||
|
|
||||||
{usersQuery.isError ? (
|
{usersQuery.isError ? (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="empty-state">
|
<EmptyState icon="alert" title="Couldn’t load users">
|
||||||
<Icon name="alert" />
|
{friendlyAuthError(usersQuery.error, 'The server did not return the user list.')}
|
||||||
<h3>Couldn’t load users</h3>
|
{' '}This tab needs the <code>rbac_users.view</code> permission.
|
||||||
<p>
|
</EmptyState>
|
||||||
{friendlyAuthError(usersQuery.error, 'The server did not return the user list.')}
|
|
||||||
{' '}This tab needs the <code>rbac_users.view</code> permission.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-wrap">
|
<div className="table-wrap">
|
||||||
|
|
@ -288,9 +281,9 @@ function Users() {
|
||||||
<td>
|
<td>
|
||||||
<div className="user-cell">
|
<div className="user-cell">
|
||||||
<Avatar name={u.name} />
|
<Avatar name={u.name} />
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="cell-primary">{u.name}</div>
|
<div className="cell-primary">{u.name}</div>
|
||||||
<div className="cell-sub">{u.email}</div>
|
<div className="cell-sub" title={u.email || undefined}>{u.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue