Replace freetext email inputs with a user lookup for tag/assign/reallocate

Typo'd addresses in the old <input type="email"> fields silently matched
nobody (assigned_to/task_tags.user_email have no FK), so a task could vanish
into an unreachable owner with no error. New UsersContext + UserPicker pull
the real user list from central auth (/api/auth/users?app=tasks), same
pattern maintenance already uses for its assignee dropdown.
This commit is contained in:
jtricerolph 2026-10-12 13:59:54 +00:00
parent 3145c1fc07
commit 6172617df3
7 changed files with 103 additions and 46 deletions

View file

@ -13,7 +13,7 @@ import { useProjects } from '../context/ProjectsContext'
import { useStatuses } from '../context/StatusesContext'
import { useAuth } from './AuthGate'
import { can } from '../types'
import { Check_, formatDateTime, toLocalInput } from './shared'
import { Check_, formatDateTime, toLocalInput, UserPicker } from './shared'
export default function TaskDetail({ taskId, onChange, onClose }:
{ taskId: number; onChange: () => void; onClose: () => void }) {
@ -226,24 +226,16 @@ export default function TaskDetail({ taskId, onChange, onClose }:
</div>
)}
{isOwner && (
<div style={{ display: 'flex', gap: 6 }}>
<input type="email" value={tagEmail} placeholder="colleague@hotel.com"
onChange={e => setTagEmail(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') addTag() }} />
<button className="btn btn-sm" onClick={addTag} disabled={!tagEmail.trim()}>Tag</button>
</div>
<UserPicker value={tagEmail} onChange={setTagEmail} onSubmit={addTag} buttonLabel="Tag"
exclude={[user.email, ...task.tags.map(tg => tg.user_email)]} />
)}
</div>
{isOwner && can(user, 'assign') && (
<div className="field" style={{ marginBottom: can(user, 'reallocate') ? 8 : 0 }}>
<label><UserPlus size={12} strokeWidth={1.75} style={{ verticalAlign: -1 }} /> Assign (hands off ownership)</label>
<div style={{ display: 'flex', gap: 6 }}>
<input type="email" value={assignEmail} placeholder={task.assigned_to ?? 'someone@hotel.com'}
onChange={e => setAssignEmail(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') assignTo() }} />
<button className="btn btn-sm" onClick={assignTo} disabled={!assignEmail.trim()}>Assign</button>
</div>
<UserPicker value={assignEmail} onChange={setAssignEmail} onSubmit={assignTo} buttonLabel="Assign"
exclude={[user.email]} />
{task.assigned_to && <div className="field-hint">Currently assigned to {task.assigned_to}</div>}
</div>
)}
@ -251,12 +243,7 @@ export default function TaskDetail({ taskId, onChange, onClose }:
{can(user, 'reallocate') && (
<div className="field" style={{ marginBottom: 0 }}>
<label><Send size={12} strokeWidth={1.75} style={{ verticalAlign: -1 }} /> Reallocate (dispatcher — moves to anyone)</label>
<div style={{ display: 'flex', gap: 6 }}>
<input type="email" value={reallocateEmail} placeholder="someone@hotel.com"
onChange={e => setReallocateEmail(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') reallocateTo() }} />
<button className="btn btn-sm" onClick={reallocateTo} disabled={!reallocateEmail.trim()}>Move</button>
</div>
<UserPicker value={reallocateEmail} onChange={setReallocateEmail} onSubmit={reallocateTo} buttonLabel="Move" />
</div>
)}