HR-ATS-Portal/frontend/src/screens/Tasks.jsx

283 lines
11 KiB
JavaScript

import { useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import Modal from '../ui/Modal'
import { Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useFormState } from '../components/AuthLayout'
import { seedQuery, useSeedMutation } from '../data/seedQueries'
import { fmtDate, fmtShort, getCandidate, savedSearches, TODAY } from '../data/seed'
const FILTERS = ['All', 'Open', 'Completed', 'Overdue', 'High', 'Medium', 'Low']
const PRIORITY_CLASS = { High: 'b-red', Medium: 'b-amber', Low: 'b-gray' }
export default function Tasks() {
const { toast } = useToast()
const navigate = useNavigate()
const { data: tasks = [] } = useQuery(seedQuery('tasks'))
const { data: recruiters = [] } = useQuery(seedQuery('recruiters'))
const updateTasks = useSeedMutation('tasks')
const [filter, setFilter] = useState('All')
const [detail, setDetail] = useState(null)
const [adding, setAdding] = useState(false)
const isOverdue = (t) => !t.done && t.due < TODAY
const list = useMemo(() => {
if (filter === 'Open') return tasks.filter((t) => !t.done)
if (filter === 'Completed') return tasks.filter((t) => t.done)
if (filter === 'Overdue') return tasks.filter(isOverdue)
if (['High', 'Medium', 'Low'].includes(filter)) return tasks.filter((t) => t.priority === filter)
return tasks
}, [tasks, filter])
const openCount = tasks.filter((t) => !t.done).length
const overdueCount = tasks.filter(isOverdue).length
// Writing to the cache is what makes the sidebar badge update — the prototype
// had to remember to call App.updateBadges() at each of these call sites.
function toggle(id) {
let nowDone = false
updateTasks((ts) =>
ts.map((t) => {
if (t.id !== id) return t
nowDone = !t.done
return { ...t, done: nowDone }
}),
)
toast(nowDone ? 'Task completed' : 'Task reopened', nowDone ? 'success' : 'info')
}
function complete(id) {
updateTasks((ts) => ts.map((t) => (t.id === id ? { ...t, done: true } : t)))
toast('Task completed', 'success')
}
return (
<div className="page">
<div className="page-head">
<div>
<h1 className="page-title">Tasks</h1>
<p className="page-sub">{openCount} open · {overdueCount} overdue</p>
</div>
<div className="page-head-actions">
<button className="btn btn-primary" onClick={() => setAdding(true)}>
<Icon name="plus" /> New Task
</button>
</div>
</div>
<div className="grid g-2-1">
<div className="card">
<div className="card-body" style={{ paddingBottom: 0 }}>
<div className="seg">
{FILTERS.map((f) => (
<button key={f} className={f === filter ? 'active' : ''} onClick={() => setFilter(f)}>
{f}
</button>
))}
</div>
</div>
<div className="card-body">
<div className="list-tight">
{list.length === 0 ? (
<EmptyState icon="check-square" title="All caught up">No tasks in this view.</EmptyState>
) : (
list.map((t) => {
const overdue = isOverdue(t)
return (
<div className="list-row" style={{ alignItems: 'center' }} key={t.id}>
<span
className={`checkbox ${t.done ? 'on' : ''}`}
onClick={(e) => { e.stopPropagation(); toggle(t.id) }}
role="checkbox"
aria-checked={t.done}
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(t.id) } }}
>
<Icon name="check" />
</span>
<div className="lr-main" style={{ cursor: 'pointer' }} onClick={() => setDetail(t)}>
<div
className="lr-title"
style={t.done ? { textDecoration: 'line-through', color: 'var(--text-3)' } : undefined}
>
{t.title}
</div>
<div className="lr-sub"><Icon name="users" /> {t.assignee} · {t.type}</div>
</div>
<div className="lr-right">
<Badge className={PRIORITY_CLASS[t.priority]}>{t.priority}</Badge>
<div
className="lr-sub"
style={{ marginTop: 4, ...(overdue ? { color: 'var(--danger)', fontWeight: 600 } : {}) }}
>
{overdue ? 'Overdue · ' : 'Due '}{fmtShort(t.due)}
</div>
</div>
</div>
)
})
)}
</div>
</div>
</div>
<div className="card" style={{ alignSelf: 'start' }}>
<div className="card-head">
<div><h3>Saved Searches</h3><span className="ch-sub">Quick candidate filters</span></div>
<button className="act-btn" onClick={() => toast('New saved search', 'info')}><Icon name="plus" /></button>
</div>
<div className="card-body">
<div className="list-tight">
{savedSearches.map((s) => (
<div key={s.name} className="list-row" style={{ cursor: 'pointer' }} onClick={() => navigate('/candidates')}>
<span className="kpi-icn i-indigo" style={{ width: 36, height: 36, borderRadius: 9 }}>
<Icon name="bookmark" />
</span>
<div className="lr-main">
<div className="lr-title">{s.name}</div>
<div className="lr-sub">{s.filters}</div>
</div>
<span className="badge b-gray badge-plain">{s.count}</span>
</div>
))}
</div>
</div>
</div>
</div>
{detail && (
<TaskDetail
task={detail}
onClose={() => setDetail(null)}
onComplete={() => { complete(detail.id); setDetail(null) }}
onViewCandidate={(id) => { setDetail(null); navigate('/candidates', { state: { openCandidate: id } }) }}
/>
)}
{adding && (
<AddTask
recruiters={recruiters}
count={tasks.length}
onClose={() => setAdding(false)}
onSave={(task) => {
updateTasks((ts) => [task, ...ts])
setAdding(false)
toast('Task created', 'success')
}}
/>
)}
</div>
)
}
function TaskDetail({ task: t, onClose, onComplete, onViewCandidate }) {
const c = t.candidateId ? getCandidate(t.candidateId) : null
return (
<Modal
title={t.title}
subtitle={`${t.id} · ${t.type}`}
onClose={onClose}
footer={
<>
<button className="btn btn-secondary" onClick={onClose}>Close</button>
{c && (
<button className="btn btn-secondary" onClick={() => onViewCandidate(c.id)}>View Candidate</button>
)}
<button className="btn btn-primary" onClick={onComplete}><Icon name="check" /> Mark Complete</button>
</>
}
>
<div className="info-grid" style={{ marginBottom: 16 }}>
<div className="info-item"><div className="il">Assignee</div><div className="iv">{t.assignee}</div></div>
<div className="info-item"><div className="il">Priority</div><div className="iv">{t.priority}</div></div>
<div className="info-item"><div className="il">Due Date</div><div className="iv">{fmtDate(t.due)}</div></div>
<div className="info-item"><div className="il">Status</div><div className="iv">{t.done ? 'Completed' : 'Open'}</div></div>
{c && <div className="info-item"><div className="il">Candidate</div><div className="iv">{c.name}</div></div>}
</div>
<div className="form-field">
<label>Notes</label>
<textarea placeholder="Add task notes…" />
</div>
</Modal>
)
}
function AddTask({ recruiters, count, onClose, onSave }) {
const form = useFormState({
title: '', priority: 'Medium', type: 'Interview',
assignee: recruiters[0]?.name ?? '', due: '',
})
function submit() {
if (!form.values.title.trim()) {
form.setErrors({ title: 'Required' })
return
}
onSave({
id: `TSK-${50001 + count}`,
title: form.values.title,
candidateId: null,
priority: form.values.priority,
due: form.values.due ? new Date(form.values.due) : new Date('2026-07-16'),
assignee: form.values.assignee,
done: false,
type: form.values.type,
})
}
return (
<Modal
title="New Task"
subtitle="Create a recruitment task"
onClose={onClose}
footer={
<>
<button className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button className="btn btn-primary" onClick={submit}><Icon name="check" /> Create Task</button>
</>
}
>
<form noValidate onSubmit={(e) => { e.preventDefault(); submit() }}>
<div className="form-grid">
<div className="form-field col-span-2">
<label>Task Title <span className="req">*</span></label>
<input
placeholder="e.g. Screen candidate"
className={form.errors.title ? 'err' : ''}
value={form.values.title}
onChange={(e) => form.setField('title', e.target.value)}
/>
<FieldError>{form.errors.title}</FieldError>
</div>
<div className="form-field">
<label>Priority</label>
<select value={form.values.priority} onChange={(e) => form.setField('priority', e.target.value)}>
<option>High</option><option>Medium</option><option>Low</option>
</select>
</div>
<div className="form-field">
<label>Type</label>
<select value={form.values.type} onChange={(e) => form.setField('type', e.target.value)}>
{['Interview', 'Review', 'Offer', 'Admin'].map((o) => <option key={o}>{o}</option>)}
</select>
</div>
<div className="form-field">
<label>Assignee</label>
<select value={form.values.assignee} onChange={(e) => form.setField('assignee', e.target.value)}>
{recruiters.map((r) => <option key={r.id}>{r.name}</option>)}
</select>
</div>
<div className="form-field">
<label>Due Date</label>
<input type="date" value={form.values.due} onChange={(e) => form.setField('due', e.target.value)} />
</div>
</div>
</form>
</Modal>
)
}