Files
plumbing-dashy/screens.jsx
T

1409 lines
58 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Screens for Dashy
function LoginScreen({ onLogin, dbUsers = [], workspace }) {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState('');
const [busy, setBusy] = React.useState(false);
const submit = async () => {
if (!username) { setError('Enter your username'); return; }
if (!password) { setError('Enter your password'); return; }
setError(''); setBusy(true);
try {
await onLogin(username, password);
} catch (e) {
setError('Incorrect username or password');
} finally {
setBusy(false);
}
};
return (
<div className="login">
<div className="login__card">
<div className="login__brand">
<BrandMark />
<span className="login__wordmark">Dashy</span>
</div>
<h1 className="login__title">Sign in to Dashy</h1>
<p className="login__sub">Enter your details to access the <span className="mono">{workspace ? workspace.name : 'loading…'}</span> workspace</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px', marginTop: '1rem' }}>
<label className="field">
<span className="field__label">Username</span>
<input
className="field__input"
value={username}
onChange={e => setUsername(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') submit(); }}
placeholder="Username"
autoFocus
/>
</label>
<label className="field">
<span className="field__label">Password</span>
<input
className="field__input"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') submit(); }}
placeholder="••••••••"
/>
</label>
</div>
{error && <div className="mono" style={{ color: 'var(--prio-high-dot)', marginTop: '0.25rem', fontSize: '12px' }}>{error}</div>}
<button className="btn btn--primary btn--full" style={{ marginTop: '0.5rem' }} onClick={submit} disabled={busy}>
{busy ? 'Signing in…' : 'Sign in'}
<Icon.Arrow />
</button>
<p className="login__foot">
<span className="mono"> </span> to submit · <a href="#" onClick={e => e.preventDefault()}>Forgot password</a>
</p>
</div>
<div className="login__side">
<div className="login__side-eyebrow mono">04 May 08 May</div>
<div className="login__side-stats">
<Stat n="42" label="tasks closed this week" />
<Stat n="14" label="auto-created from email" />
<Stat n="6" label="from iMessage → Dashy" />
<Stat n="2" label="form responses re-routed" />
</div>
<div className="login__side-foot mono">v0.1 · build 26.05.08</div>
</div>
</div>
);
}
function Stat({ n, label }) {
return (
<div className="stat">
<div className="stat__n">{n}</div>
<div className="stat__label">{label}</div>
</div>
);
}
function BrandMark({ size = 22 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect x="2" y="2" width="9" height="13" rx="2" fill="currentColor" />
<rect x="13" y="2" width="9" height="6" rx="2" fill="currentColor" opacity="0.55" />
<rect x="2" y="17" width="9" height="5" rx="2" fill="currentColor" opacity="0.55" />
<rect x="13" y="10" width="9" height="12" rx="2" fill="currentColor" opacity="0.85" />
</svg>
);
}
function TopBar({ me, dbUsers = [], isAdmin, tab, setTab, onAdd, onLogs, onLogout, onProfile, workspace, searchQuery, setSearchQuery, showSearch, onToggleSearch }) {
return (
<header className="topbar">
<div className="topbar__left">
{showSearch ? (
<div className="topbar__search">
<Icon.Search />
<input
className="topbar__search-input"
autoFocus
placeholder="Search tasks, descriptions, tags…"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
<IconBtn label="Close search" onClick={onToggleSearch}>
<Icon.Close />
</IconBtn>
</div>
) : (
<>
<span className="topbar__brand"><BrandMark /><span>Dashy</span></span>
<span className="topbar__divider" />
<span className="topbar__workspace">{workspace ? workspace.name : 'loading…'}</span>
</>
)}
</div>
{!showSearch && (
<nav className="tabs" role="tablist">
<Tab id="overview" label="Overview" tab={tab} setTab={setTab} />
{dbUsers.map(u => (
<Tab key={u.id} id={u.id} label={u.name} tab={tab} setTab={setTab} user={u} />
))}
{isAdmin && <Tab id="deleted" label="Deleted" tab={tab} setTab={setTab} />}
</nav>
)}
<div className="topbar__right">
{!showSearch && (
<>
<button className="btn btn--soft" onClick={onAdd}>
<Icon.Plus /> New task
</button>
<IconBtn label="Search" onClick={onToggleSearch}>
<Icon.Search />
</IconBtn>
</>
)}
{isAdmin && (
<IconBtn label="Audit log" onClick={onLogs}>
<Icon.Logs />
</IconBtn>
)}
<button className="topbar__me" onClick={onProfile}>
<Avatar user={me} size={28} ring />
<div className="topbar__me-meta">
<span className="topbar__me-name">
{me.name}
{isAdmin && <span className="admin-badge" title="Admin"></span>}
</span>
<span className="topbar__me-role">{isAdmin ? 'Admin · ' : ''}{me.role}</span>
</div>
</button>
</div>
</header>
);
}
function Tab({ id, label, tab, setTab, user }) {
const active = tab === id;
return (
<button
role="tab"
aria-selected={active}
className={"tab" + (active ? " is-active" : "")}
onClick={() => setTab(id)}
>
{user && <span className="tab__dot" style={{ background: `oklch(70% 0.10 ${user.hue})` }} />}
{label}
</button>
);
}
function HeadsUp({ items, onDismiss, onOpenTask }) {
if (!items.length) return null;
return (
<div className="heads-up" role="status">
{items.map(it => (
<div key={it.id} className={"heads-up__item heads-up__item--" + it.kind}>
<span className="heads-up__icon">
{it.kind === 'unsuccessful' ? '!' : it.kind === 'billing' ? '⇄' : '✦'}
</span>
<div className="heads-up__body">
<div className="heads-up__title">{it.title}</div>
<div className="heads-up__sub">{it.sub}</div>
</div>
<button className="heads-up__open" onClick={() => onOpenTask(it.taskId)}>
View <Icon.Arrow />
</button>
<button className="heads-up__close" onClick={() => onDismiss(it.id)} aria-label="Dismiss">
<Icon.Close />
</button>
</div>
))}
</div>
);
}
function OverviewScreen({ tasks, onOpen, onAddFor, density, onMoveTask, dbUsers = [] }) {
const byUser = Object.fromEntries(dbUsers.map(u => [u.id, []]));
tasks.forEach(t => { if (byUser[t.assignee] && t.status !== 'closed') byUser[t.assignee].push(t); });
const [draggingTask, setDraggingTask] = React.useState(null);
const [dragOverCol, setDragOverCol] = React.useState(null);
const [dragOverTaskId, setDragOverTaskId] = React.useState(null);
const [dropSide, setDropSide] = React.useState('bottom'); // 'top' or 'bottom'
return (
<div className="board">
{dbUsers.map(u => (
<Column
key={u.id}
user={u}
// Filter out the dragging task from its current column to make it "disappear" from origin
tasks={byUser[u.id].filter(t => !draggingTask || t.id !== draggingTask.id)}
onOpen={onOpen}
onAdd={() => onAddFor(u.id)}
density={density}
dragOver={dragOverCol === u.id && draggingTask && draggingTask.assignee !== u.id}
onDragOver={(uid) => setDragOverCol(uid)}
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
onDragStartCard={(t) => {
// Use a tiny timeout to ensure the drag image is captured before we hide the element
setTimeout(() => setDraggingTask(t), 0);
}}
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
draggingId={draggingTask && draggingTask.id}
dragOverTaskId={dragOverTaskId}
dropSide={dropSide}
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
onDropTask={(toId) => {
if (!draggingTask) return;
// Calculate position using the list that DOES NOT include the dragging task
const colTasks = byUser[toId].filter(t => t.id !== draggingTask.id);
let newPos = 0;
if (dragOverTaskId) {
const idx = colTasks.findIndex(t => t.id === dragOverTaskId);
if (dropSide === 'top') {
const prev = colTasks[idx - 1];
newPos = prev ? (colTasks[idx].position + prev.position) / 2 : colTasks[idx].position / 2;
} else {
const next = colTasks[idx + 1];
newPos = next ? (colTasks[idx].position + next.position) / 2 : colTasks[idx].position + 1000;
}
} else {
// Dropped on empty area or no specific task
const last = colTasks[colTasks.length - 1];
newPos = last ? last.position + 1000 : 1000;
}
onMoveTask && onMoveTask(draggingTask.id, toId, newPos);
setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null);
}}
/>
))}
</div>
);
}
function Column({ user, title, icon, tasks, onOpen, onAdd, density, onDropTask, dragOver, onDragOver, onDragLeave, onDragStartCard, onDragEndCard, draggingId, dragOverTaskId, dropSide, onDragOverTask, colId, faded }) {
const columnId = colId || (user ? user.id : title);
return (
<section
className={"column" + (dragOver && tasks.length === 0 ? " column--over" : "")}
onDragOver={(e) => { e.preventDefault(); onDragOver && onDragOver(columnId); }}
onDragLeave={(e) => {
if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget)) {
onDragLeave && onDragLeave(columnId);
}
}}
onDrop={(e) => { e.preventDefault(); onDropTask && onDropTask(columnId); }}
>
<header className="column__head">
<div className="column__head-l">
{user ? (
<>
<Avatar user={user} size={28} />
<div className="column__head-meta">
<h2 className="column__name">{user.name}</h2>
<span className="column__role">{user.role}</span>
</div>
</>
) : (
<>
{icon && <span className="column__icon">{icon}</span>}
<div className="column__head-meta">
<h2 className="column__name">{title}</h2>
</div>
</>
)}
</div>
<div className="column__head-r">
<span className="column__count mono">{tasks.length}</span>
{onAdd && (
<button className="add-btn" onClick={onAdd} aria-label={"Add task"}>
<Icon.Plus />
</button>
)}
</div>
</header>
<div className="column__list" style={{ flex: 1, position: 'relative', opacity: faded ? 0.45 : 1 }}
onDragOver={(e) => {
e.preventDefault();
if (e.target === e.currentTarget) {
onDragOverTask(null, 'bottom');
}
}}>
{tasks.length === 0 && !dragOver && (
<div className="column__empty">
<span className="mono"> inbox zero </span>
</div>
)}
{tasks.length === 0 && dragOver && (
<div className="drop-placeholder" />
)}
{tasks.map(t => {
const isOver = dragOverTaskId === t.id;
const isTop = isOver && dropSide === 'top';
const isBottom = isOver && dropSide === 'bottom';
return (
<div key={t.id} style={{
position: 'relative',
transition: 'padding 0.18s cubic-bezier(0.2, 0.8, 0.2, 1)',
paddingTop: isTop ? '60px' : '0px',
paddingBottom: isBottom ? '60px' : '0px'
}}>
{isOver && (
<div className="drop-placeholder" style={{
position: 'absolute',
left: 0, right: 0,
top: isTop ? '4px' : 'auto',
bottom: isBottom ? '4px' : 'auto',
margin: 0
}} />
)}
<TaskCard
task={t}
onOpen={onOpen}
density={density}
dragging={draggingId === t.id}
onDragStart={onDragStartCard}
onDragEnd={onDragEndCard}
onDragOver={(e) => {
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
onDragOverTask(t.id, e.clientY < mid ? 'top' : 'bottom');
}}
/>
</div>
);
})}
</div>
</section>
);
}
function UserScreen({ user, tasks, onOpen, onAddFor, density, onMoveTask }) {
const mine = tasks.filter(t => t.assignee === user.id);
const open = mine.filter(t => t.status === 'open');
const flagged = mine.filter(t => t.status === 'unsuccessful' || t.status === 'billing');
const closed = mine.filter(t => t.status === 'closed');
const [draggingTask, setDraggingTask] = React.useState(null);
const [dragOverCol, setDragOverCol] = React.useState(null);
const [dragOverTaskId, setDragOverTaskId] = React.useState(null);
const [dropSide, setDropSide] = React.useState('bottom');
const onDrop = (status) => {
if (!draggingTask) return;
const targetTasks = status === 'flagged' ? flagged : (status === 'open' ? open : closed);
const cleanTarget = targetTasks.filter(t => t.id !== draggingTask.id);
let newPos = 0;
if (dragOverTaskId) {
const idx = cleanTarget.findIndex(t => t.id === dragOverTaskId);
if (dropSide === 'top') {
const prev = cleanTarget[idx - 1];
newPos = prev ? (cleanTarget[idx].position + prev.position) / 2 : cleanTarget[idx].position / 2;
} else {
const next = cleanTarget[idx + 1];
newPos = next ? (cleanTarget[idx].position + next.position) / 2 : cleanTarget[idx].position + 1000;
}
} else {
const last = cleanTarget[cleanTarget.length - 1];
newPos = last ? last.position + 1000 : 1000;
}
// Determine target status
let targetStatus = status;
if (status === 'flagged') targetStatus = 'unsuccessful'; // Default flagged status
if (status === 'closed') targetStatus = 'closed';
if (status === 'open') targetStatus = 'open';
onMoveTask && onMoveTask(draggingTask.id, user.id, newPos, targetStatus);
setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null);
};
return (
<div className="user-view">
<div className="user-view__hero">
<Avatar user={user} size={64} />
<div className="user-view__hero-meta">
<h1 className="user-view__name">{user.name}</h1>
<p className="user-view__role">{user.role} · <span className="mono">{open.length + flagged.length} open tasks</span></p>
</div>
<div className="user-view__hero-cta">
<button className="btn btn--primary" onClick={() => onAddFor(user.id)}>
<Icon.Plus /> New task for {user.name}
</button>
</div>
</div>
<div className="board board--user">
<Column
title="Needs review" icon="⚠" colId="flagged"
tasks={flagged.filter(t => !draggingTask || t.id !== draggingTask.id)}
onOpen={onOpen} density={density}
dragOver={dragOverCol === 'flagged'}
onDragOver={setDragOverCol}
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
onDragStartCard={(t) => {
setTimeout(() => setDraggingTask(t), 0);
}}
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
draggingId={draggingTask?.id}
dragOverTaskId={dragOverTaskId} dropSide={dropSide}
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
onDropTask={onDrop}
/>
<Column
title="Open" icon="○" colId="open"
tasks={open.filter(t => !draggingTask || t.id !== draggingTask.id)}
onOpen={onOpen} density={density}
dragOver={dragOverCol === 'open'}
onDragOver={setDragOverCol}
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
onDragStartCard={(t) => {
setTimeout(() => setDraggingTask(t), 0);
}}
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
draggingId={draggingTask?.id}
dragOverTaskId={dragOverTaskId} dropSide={dropSide}
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
onDropTask={onDrop}
/>
<Column
title="Completed" icon="✓" colId="closed" faded={true}
tasks={closed.filter(t => !draggingTask || t.id !== draggingTask.id)}
onOpen={onOpen} density={density}
dragOver={dragOverCol === 'closed'}
onDragOver={setDragOverCol}
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
onDragStartCard={(t) => {
setTimeout(() => setDraggingTask(t), 0);
}}
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
draggingId={draggingTask?.id}
dragOverTaskId={dragOverTaskId} dropSide={dropSide}
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
onDropTask={onDrop}
/>
</div>
</div>
);
}
function Section({ title, sub, children }) {
return (
<section className="section">
<header className="section__head">
<h2 className="section__title">{title}</h2>
{sub && <span className="section__sub mono">{sub}</span>}
</header>
{children}
</section>
);
}
function AddTaskModal({ open, onClose, onSubmit, defaultAssignee, me, dbUsers = [] }) {
const [title, setTitle] = React.useState('');
const [desc, setDesc] = React.useState('');
const [assignee, setAssignee] = React.useState(defaultAssignee || 'lani');
const [priority, setPriority] = React.useState('med');
React.useEffect(() => { if (open) setAssignee(defaultAssignee || 'lani'); }, [open, defaultAssignee]);
React.useEffect(() => { if (open) { setTitle(''); setDesc(''); setPriority('med'); } }, [open]);
if (!open) return null;
const submit = (e) => {
e && e.preventDefault();
if (!title.trim()) return;
onSubmit({ title: title.trim(), description: desc.trim(), assignee, priority });
};
return (
<Modal onClose={onClose} title="New task" eyebrow={"From " + me.name}>
<form onSubmit={submit} className="modal__form">
<label className="field">
<span className="field__label">Task</span>
<input
className="field__input field__input--big"
autoFocus
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="What needs doing?"
/>
</label>
<label className="field">
<span className="field__label">Notes <span className="mono"> optional</span></span>
<textarea
className="field__textarea"
rows={3}
value={desc}
onChange={e => setDesc(e.target.value)}
placeholder="Any context, links, work-order numbers…"
/>
</label>
<div className="modal__row">
<div className="field">
<span className="field__label">Assign to</span>
<div className="picker">
{dbUsers.map(u => (
<button
type="button"
key={u.id}
className={"picker__item" + (assignee === u.id ? " is-on" : "")}
onClick={() => setAssignee(u.id)}
>
<Avatar user={u} size={20} />
{u.name}
</button>
))}
</div>
</div>
<div className="field">
<span className="field__label">Priority</span>
<div className="picker">
{Object.entries(PRIORITY).map(([k, v]) => (
<button
type="button"
key={k}
className={"picker__item" + (priority === k ? " is-on" : "")}
onClick={() => setPriority(k)}
>
<span className="prio__dot" style={{ background: v.dot }} />
{v.label}
</button>
))}
</div>
</div>
</div>
<footer className="modal__foot">
<p className="modal__hint mono">
tip say <span className="kbd">"Hey Siri, message Dashy"</span> to add a task from your phone
</p>
<div className="modal__actions">
<button type="button" className="btn btn--ghost" onClick={onClose}>Cancel</button>
<button type="submit" className="btn btn--primary" disabled={!title.trim()}>
Add task <Icon.Arrow />
</button>
</div>
</footer>
</form>
</Modal>
);
}
function Modal({ children, onClose, title, eyebrow, wide = false }) {
return (
<div className="modal-scrim" onClick={onClose}>
<div className={"modal" + (wide ? " modal--wide" : "")} onClick={e => e.stopPropagation()}>
<header className="modal__head">
<div>
{eyebrow && <span className="modal__eyebrow mono">{eyebrow}</span>}
<h2 className="modal__title">{title}</h2>
</div>
<IconBtn label="Close" onClick={onClose}><Icon.Close /></IconBtn>
</header>
{children}
</div>
</div>
);
}
function TaskDetail({ task, allAudit = [], onClose, onMove, onPriority, onComplete, onReopen, onEditDesc, onDeleteTask, onAddNote }) {
const [editingDesc, setEditingDesc] = React.useState(false);
const [descValue, setDescValue] = React.useState(task ? task.description || '' : '');
const [noteValue, setNoteValue] = React.useState('');
React.useEffect(() => {
setDescValue(task ? task.description || '' : '');
}, [task]);
if (!task) return null;
const assignee = findUser(task.assignee);
const author = findUser(task.addedBy);
const audit = allAudit.filter(a => a.target === task.id);
if (audit.length === 0) {
audit.push({ at: task.addedAt, actor: task.addedBy, action: 'task_created', summary: '' });
}
const handleAddNote = async () => {
if (!noteValue.trim()) return;
await onAddNote(noteValue);
setNoteValue('');
};
return (
<Modal onClose={onClose} wide title={task.title} eyebrow={(task.tags && task.tags.join(' · ')) || 'Task'}>
<div className="detail">
<div className="detail__main">
{task.status === 'unsuccessful' && (
<div className="detail__alert detail__alert--warn">
<strong>Auto-marked Unsuccessful.</strong> Two missed bookings detected by the scheduler. Decide on next step or revert.
<div className="detail__alert-actions">
<button className="btn btn--ghost btn--sm">Revert to Open</button>
<button className="btn btn--soft btn--sm" onClick={onComplete}>Close as unsuccessful</button>
</div>
</div>
)}
{task.status === 'billing' && (
<div className="detail__alert detail__alert--info">
<strong>Form re-routed to Billing.</strong> Originally captured as Service Booking keywords matched billing template.
</div>
)}
<div className="detail__desc-section" style={{ marginBottom: '1.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<h3 className="detail__h" style={{ margin: 0 }}>Description</h3>
{!editingDesc && (
<IconBtn label="Edit description" onClick={() => setEditingDesc(true)}>
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M12.5 3.5l-8 8V14h2.5l8-8-2.5-2.5z" strokeLinecap="round" strokeLinejoin="round"/><path d="M10.5 5.5l2 2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</IconBtn>
)}
</div>
{editingDesc ? (
<div className="detail__desc-edit">
<textarea
className="field__textarea"
value={descValue}
onChange={e => setDescValue(e.target.value)}
autoFocus
rows={4}
/>
<div className="detail__alert-actions" style={{ marginTop: '0.5rem' }}>
<button className="btn btn--ghost btn--sm" onClick={() => { setDescValue(task.description || ''); setEditingDesc(false); }}>Cancel</button>
<button className="btn btn--primary btn--sm" onClick={() => { onEditDesc(descValue); setEditingDesc(false); }}>Save</button>
</div>
</div>
) : (
<p className="detail__desc" style={{ marginTop: 0 }}>{task.description || <span className="mono" style={{opacity: 0.5}}>No description</span>}</p>
)}
</div>
<div className="detail__notes">
<h3 className="detail__h">Notes</h3>
<ul className="notes">
{task.notes && task.notes.map(note => {
const noteAuthor = findUser(note.author_id);
return (
<li key={note.id} className="notes__item">
<span className="notes__bullet"><Icon.Pin /></span>
<div>
<div>{note.body}</div>
<div className="notes__meta mono">
{noteAuthor ? noteAuthor.name : 'Unknown'} · {fmtDateTime(note.created_at)}
</div>
</div>
</li>
);
})}
</ul>
<div className="notes__add">
<input
className="field__input"
placeholder="Add a note…"
value={noteValue}
onChange={e => setNoteValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleAddNote(); }}
/>
<button className="btn btn--soft btn--sm" onClick={handleAddNote}>Add</button>
</div>
</div>
<div className="detail__audit">
<h3 className="detail__h">Activity</h3>
<ol className="timeline">
{audit.slice().reverse().map((row, i) => (
<li key={i} className="timeline__row">
<div className="timeline__rail">
<span className="timeline__dot" data-actor={row.actor} />
</div>
<div className="timeline__body">
<div className="timeline__line">
<strong>
{row.actor === 'system' ? 'System' : (findUser(row.actor) || {}).name}
</strong>
<span> {row.action.replace(/_/g, ' ')}</span>
</div>
{(row.detail || row.summary) && <div className="timeline__detail">{row.detail || row.summary}</div>}
<div className="timeline__time mono">{fmtDateTime(row.at)}</div>
</div>
</li>
))}
</ol>
</div>
</div>
<aside className="detail__side">
{task.status === 'closed' ? (
<button className="btn btn--ghost btn--full" style={{ marginBottom: '1.5rem' }} onClick={onReopen}>
<Icon.Arrow /> Reopen task
</button>
) : (
<button className="btn btn--primary btn--full" style={{ marginBottom: '1.5rem' }} onClick={onComplete}>
<Icon.Check /> Mark as completed
</button>
)}
<Field label="Assigned to">
<div className="picker">
{dbUsers.map(u => (
<button key={u.id}
className={"picker__item" + (task.assignee === u.id ? " is-on" : "")}
onClick={() => onMove(task.id, u.id)}>
<Avatar user={u} size={20} />
{u.name}
</button>
))}
</div>
</Field>
<Field label="Priority">
<div className="picker">
{Object.entries(PRIORITY).map(([k, v]) => (
<button key={k}
className={"picker__item" + (task.priority === k ? " is-on" : "")}
onClick={() => onPriority(task.id, k)}>
<span className="prio__dot" style={{ background: v.dot }} />
{v.label}
</button>
))}
</div>
</Field>
<Field label="Source">
<div className="meta-row">
{task.source === 'imessage' && <><Icon.iMessage /> iMessage from {author && author.name}</>}
{task.source === 'email' && <><span className="mono"></span> Auto-created from email</>}
{task.source === 'automation' && <><span className="mono"></span> System automation</>}
{task.source === 'manual' && <>Created by {author && author.name}</>}
</div>
</Field>
<Field label="Added">
<div className="meta-row mono">{fmtDateTime(task.addedAt)}</div>
</Field>
<Field label="Reminder">
<div className="meta-row">Thu, May 7 · 4:00 pm</div>
</Field>
<div style={{ marginTop: '2rem', paddingTop: '1.5rem', borderTop: '1px solid var(--border-subtle)' }}>
<button
className="btn btn--ghost btn--full"
style={{ color: 'var(--prio-high-dot)', borderColor: 'transparent' }}
onClick={() => { if (confirm('Are you sure you want to permanently delete this task?')) onDeleteTask(); }}
>
<Icon.Close /> Delete task permanently
</button>
</div>
</aside>
</div>
</Modal>
);
}
function Field({ label, children }) {
return (
<div className="side-field">
<div className="side-field__label mono">{label}</div>
<div className="side-field__body">{children}</div>
</div>
);
}
function AuditScreen({ entries, onOpen }) {
const [actorFilter, setActorFilter] = React.useState('all');
const [eventFilter, setEventFilter] = React.useState('all');
const [auditSearch, setAuditSearch] = React.useState('');
const EVENT_TYPES = [
{ value: 'all', label: 'All events' },
{ value: 'task', label: 'Tasks (all)' },
{ value: 'task_created', label: 'Created' },
{ value: 'task_moved', label: 'Moved' },
{ value: 'task_completed', label: 'Completed' },
{ value: 'task_deleted', label: 'Deleted' },
{ value: 'task_restored', label: 'Restored' },
{ value: 'user', label: 'User management' },
{ value: 'workspace_updated', label: 'Workspace' },
];
const groups = React.useMemo(() => {
let filtered = entries;
// 1. Actor Filter
if (actorFilter !== 'all') {
filtered = actorFilter === 'system'
? filtered.filter(e => e.actor === 'system')
: filtered.filter(e => e.actor === actorFilter);
}
// 2. Event Type Filter
if (eventFilter !== 'all') {
if (eventFilter === 'task') {
filtered = filtered.filter(e => e.action.startsWith('task_'));
} else if (eventFilter === 'user') {
filtered = filtered.filter(e => e.action.startsWith('user_') || e.action === 'password_changed');
} else {
filtered = filtered.filter(e => e.action === eventFilter);
}
}
// 3. Search Query
if (auditSearch.trim()) {
const q = auditSearch.toLowerCase();
filtered = filtered.filter(e =>
e.summary.toLowerCase().includes(q) ||
e.action.toLowerCase().includes(q) ||
(e.actor !== 'system' && (findUser(e.actor) || {}).name?.toLowerCase().includes(q))
);
}
const out = {};
filtered.forEach(e => {
const day = new Date(e.at).toDateString();
(out[day] = out[day] || []).push(e);
});
return out;
}, [actorFilter, eventFilter, auditSearch, entries]);
return (
<div className="audit">
<header className="audit__head" style={{ gap: '20px' }}>
<div style={{ flex: 1, minWidth: '300px' }}>
<h1 className="audit__title">Audit log</h1>
<p className="audit__sub">History of the workspace · <span className="mono">last 7 days</span></p>
<div style={{ marginTop: '1rem', display: 'flex', gap: '8px' }}>
<div className="topbar__search" style={{ maxWidth: 'none', flex: 1 }}>
<Icon.Search />
<input
className="topbar__search-input"
placeholder="Search audit trail…"
value={auditSearch}
onChange={e => setAuditSearch(e.target.value)}
/>
{auditSearch && <IconBtn label="Clear" onClick={() => setAuditSearch('')}><Icon.Close /></IconBtn>}
</div>
<select
className="field__input"
style={{ width: '180px', padding: '0 8px', height: '32px', fontSize: '12.5px' }}
value={eventFilter}
onChange={e => setEventFilter(e.target.value)}
>
{EVENT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
</div>
</div>
<div className="audit__filter" style={{ alignSelf: 'flex-end' }}>
<FilterChip on={actorFilter==='all'} onClick={() => setActorFilter('all')}>All actors</FilterChip>
<FilterChip on={actorFilter==='system'} onClick={() => setActorFilter('system')}>System</FilterChip>
{dbUsers.map(u => (
<FilterChip key={u.id} on={actorFilter===u.id} onClick={() => setActorFilter(u.id)}>
<Avatar user={u} size={16} /> {u.name}
</FilterChip>
))}
</div>
</header>
<div className="audit__list">
{Object.entries(groups).map(([day, rows]) => (
<div key={day} className="audit__group">
<div className="audit__day mono">{day}</div>
<ul className="audit__rows">
{rows.map(r => {
const actor = r.actor === 'system' ? null : findUser(r.actor);
return (
<li key={r.id} className="audit__row">
<span className="audit__time mono">{new Date(r.at).toLocaleTimeString('en-US',{hour:'numeric', minute:'2-digit'})}</span>
<span className="audit__actor">
{actor ? <Avatar user={actor} size={20} /> : <span className="audit__sys">SYS</span>}
<span>{actor ? actor.name : 'System'}</span>
</span>
<span className="audit__action mono">{r.action.replace(/_/g, ' ')}</span>
<span className="audit__summary">{r.summary}</span>
{r.target && (
<button className="audit__link" onClick={() => onOpen(r.target)}>open <Icon.Arrow /></button>
)}
</li>
);
})}
</ul>
</div>
))}
</div>
</div>
);
}
function FilterChip({ on, onClick, children }) {
return (
<button className={"filter-chip" + (on ? " is-on" : "")} onClick={onClick}>
{children}
</button>
);
}
function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onSwitchUser, onCreateUser, onDeleteUser, onUpdateUserRole, onChangePassword, workspace, onUpdateWorkspace }) {
const [name, setName] = React.useState(user.name);
const [role, setRole] = React.useState(user.role);
const [email, setEmail] = React.useState(user.email || '');
const [phone, setPhone] = React.useState(user.phone || '');
const [photo, setPhoto] = React.useState(user.photo || null);
const [tab, setTab] = React.useState('profile');
const [pwOld, setPwOld] = React.useState('');
const [pwNew, setPwNew] = React.useState('');
const [pwConfirm, setPwConfirm] = React.useState('');
const [pwSaved, setPwSaved] = React.useState(false);
const [pwError, setPwError] = React.useState('');
const [pwBusy, setPwBusy] = React.useState(false);
const [saved, setSaved] = React.useState(false);
const submitPasswordChange = async () => {
setPwError('');
if (pwNew !== pwConfirm) { setPwError('New passwords do not match'); return; }
setPwBusy(true);
try {
await onChangePassword(pwOld, pwNew);
setPwOld(''); setPwNew(''); setPwConfirm('');
setPwSaved(true);
setTimeout(() => setPwSaved(false), 2000);
} catch (e) {
setPwError(e.message || 'Failed to update password');
} finally {
setPwBusy(false);
}
};
React.useEffect(() => {
setName(user.name);
setRole(user.role);
setEmail(user.email || '');
setPhone(user.phone || '');
setPhoto(user.photo || null);
}, [user.id]);
const fileInputRef = React.useRef(null);
const handleFile = (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = ev => setPhoto(ev.target.result);
reader.readAsDataURL(f);
};
const save = () => {
onSave({ name, role, photo, email, phone });
setSaved(true);
setTimeout(() => setSaved(false), 1600);
};
const dirty = name !== user.name || role !== user.role || photo !== (user.photo || null) || email !== (user.email || '') || phone !== (user.phone || '');
return (
<Modal onClose={onClose} title="Account & settings" eyebrow={"Signed in as " + user.name} wide>
<div className="settings">
<nav className="settings__nav">
<button className={"settings__nav-item" + (tab==='profile'?' is-on':'')} onClick={() => setTab('profile')}>
<span className="settings__nav-icon"></span> Profile
</button>
<button className={"settings__nav-item" + (tab==='security'?' is-on':'')} onClick={() => setTab('security')}>
<span className="settings__nav-icon"></span> Password
</button>
<button className={"settings__nav-item" + (tab==='notifications'?' is-on':'')} onClick={() => setTab('notifications')}>
<span className="settings__nav-icon"></span> Notifications
</button>
<button className={"settings__nav-item" + (tab==='workspace'?' is-on':'')} onClick={() => setTab('workspace')}>
<span className="settings__nav-icon"></span> Workspace
</button>
<div className="settings__nav-foot">
<button className="settings__logout" onClick={onLogout}>
<Icon.Arrow /> Log out
</button>
</div>
</nav>
<div className="settings__body">
{tab === 'profile' && (
<>
<h3 className="settings__h">Profile picture</h3>
<div className="settings__photo">
<div className="settings__photo-preview" style={photo ? {backgroundImage:`url(${photo})`} : {}}>
{!photo && <Avatar user={{...user, name, initials: (name||'??').split(' ').map(s=>s[0]).join('').slice(0,2).toUpperCase()}} size={88} />}
</div>
<div className="settings__photo-actions">
<input ref={fileInputRef} type="file" accept="image/*" hidden onChange={handleFile} />
<button className="btn btn--soft btn--sm" onClick={() => fileInputRef.current && fileInputRef.current.click()}>
Upload photo
</button>
{photo && (
<button className="btn btn--ghost btn--sm" onClick={() => setPhoto(null)}>
Remove
</button>
)}
<p className="settings__hint mono">PNG or JPG, square crop recommended · max 2MB</p>
</div>
</div>
<h3 className="settings__h">Personal details</h3>
<div className="settings__grid">
<label className="field">
<span className="field__label">Display name</span>
<input className="field__input" value={name} onChange={e => setName(e.target.value)} />
</label>
<label className="field">
<span className="field__label">Role / title {!isAdmin && <span className="mono lock"> admin only</span>}</span>
<input className="field__input" value={role} onChange={e => setRole(e.target.value)} disabled={!isAdmin} />
</label>
<label className="field">
<span className="field__label">Email</span>
<input className="field__input" value={email} onChange={e => setEmail(e.target.value)} placeholder="yourname@example.com" />
</label>
<label className="field">
<span className="field__label">Phone</span>
<input className="field__input" value={phone} onChange={e => setPhone(e.target.value)} placeholder="+64 ..." />
</label>
</div>
<div className="settings__save-row">
{saved && <span className="settings__saved mono"><Icon.Check /> Saved</span>}
<button className="btn btn--ghost" onClick={() => { setName(user.name); setRole(user.role); setPhoto(user.photo||null); setEmail(user.email||''); setPhone(user.phone||''); }} disabled={!dirty}>Discard</button>
<button className="btn btn--primary" onClick={save} disabled={!dirty}>Save changes</button>
</div>
</>
)}
{tab === 'security' && (
<>
<h3 className="settings__h">Change password</h3>
<div className="settings__col">
<label className="field">
<span className="field__label">Current password</span>
<input className="field__input" type="password" value={pwOld} onChange={e => setPwOld(e.target.value)} placeholder="••••••••" />
</label>
<label className="field">
<span className="field__label">New password</span>
<input className="field__input" type="password" value={pwNew} onChange={e => setPwNew(e.target.value)} />
</label>
<label className="field">
<span className="field__label">Confirm new password</span>
<input className="field__input" type="password" value={pwConfirm} onChange={e => setPwConfirm(e.target.value)} />
</label>
<div className="settings__pw-strength">
<span className="settings__pw-bar" data-strength={pwNew.length > 11 ? 'strong' : pwNew.length > 7 ? 'med' : pwNew.length > 0 ? 'weak' : 'none'} />
<span className="mono settings__pw-label">
{pwNew.length === 0 ? 'enter a new password' : pwNew.length > 11 ? 'strong' : pwNew.length > 7 ? 'medium — add a symbol or number' : 'weak — needs more characters'}
</span>
</div>
</div>
{pwError && <div className="settings__pw-error mono" style={{ color: 'var(--prio-high-dot)', marginTop: '0.5rem' }}>{pwError}</div>}
<div className="settings__save-row">
{pwSaved && <span className="settings__saved mono"><Icon.Check /> Password updated</span>}
<button className="btn btn--ghost" onClick={() => { setPwOld(''); setPwNew(''); setPwConfirm(''); setPwError(''); }}>Cancel</button>
<button className="btn btn--primary" disabled={!pwOld || !pwNew || pwNew !== pwConfirm || pwBusy} onClick={submitPasswordChange}>{pwBusy ? 'Updating…' : 'Update password'}</button>
</div>
<div className="settings__divider" />
<h3 className="settings__h">Sign-in &amp; sessions</h3>
<ul className="settings__sessions">
<li className="settings__session">
<div>
<div className="settings__session-where">MacBook · Chrome · this device</div>
<div className="settings__session-when mono">Active now · Christchurch, NZ</div>
</div>
<span className="chip">current</span>
</li>
<li className="settings__session">
<div>
<div className="settings__session-where">iPhone · Safari</div>
<div className="settings__session-when mono">Last active 2h ago</div>
</div>
<button className="btn btn--ghost btn--sm">Sign out</button>
</li>
</ul>
</>
)}
{tab === 'notifications' && (
<>
<h3 className="settings__h">When should we ping you?</h3>
<ul className="settings__toggles">
<ToggleRow label="A new task is assigned to me" defaultOn />
<ToggleRow label="Someone @mentions me in a note" defaultOn />
<ToggleRow label="A task I created is completed" />
<ToggleRow label="Daily digest at 8am" defaultOn />
<ToggleRow label="Heads-up alerts (auto-flagged tasks)" defaultOn />
</ul>
<h3 className="settings__h">Channels</h3>
<ul className="settings__toggles">
<ToggleRow label="In-app heads-up" defaultOn />
<ToggleRow label="Push to phone (Dashy app)" defaultOn />
<ToggleRow label="Email" />
<ToggleRow label="iMessage relay" />
</ul>
</>
)}
{tab === 'workspace' && (
<WorkspaceTab
user={user} isAdmin={isAdmin} dbUsers={dbUsers}
onSwitchUser={onSwitchUser}
onCreateUser={onCreateUser}
onDeleteUser={onDeleteUser}
onUpdateUserRole={onUpdateUserRole}
workspace={workspace}
onUpdateWorkspace={onUpdateWorkspace}
/>
)}
</div>
</div>
</Modal>
);
}
function ToggleRow({ label, defaultOn = false }) {
const [on, setOn] = React.useState(defaultOn);
return (
<li className="toggle-row">
<span>{label}</span>
<button className={"switch" + (on ? " is-on" : "")} onClick={() => setOn(v => !v)} aria-pressed={on}>
<span className="switch__thumb" />
</button>
</li>
);
}
function WorkspaceTab({ user, isAdmin, dbUsers = [], onSwitchUser, onCreateUser, onDeleteUser, onUpdateUserRole, workspace, onUpdateWorkspace }) {
const [adding, setAdding] = React.useState(false);
const [newName, setNewName] = React.useState('');
const [newRole, setNewRole] = React.useState('');
const [newType, setNewType] = React.useState('standard');
const [wsName, setWsName] = React.useState(workspace ? workspace.name : '');
const [wsTz, setWsTz] = React.useState(workspace ? workspace.timezone : '');
const [wsSaved, setWsSaved] = React.useState(false);
// User editing state
const [editingUserId, setEditingUserId] = React.useState(null);
const [editName, setEditName] = React.useState('');
const [editRole, setEditRole] = React.useState('');
const startEditing = (u) => {
setEditingUserId(u.id);
setEditName(u.name);
setEditRole(u.role);
};
const cancelEditing = () => {
setEditingUserId(null);
};
const saveUserEdit = async (id) => {
await onUpdateUserRole(id, { name: editName, role: editRole });
setEditingUserId(null);
};
React.useEffect(() => {
if (workspace) {
setWsName(workspace.name);
setWsTz(workspace.timezone);
}
}, [workspace]);
const submit = () => {
if (!newName.trim()) return;
onCreateUser({ name: newName.trim(), role: newRole.trim() || 'Team member', account_type: newType });
setNewName(''); setNewRole(''); setNewType('standard'); setAdding(false);
};
const handleUpdateWorkspace = async () => {
await onUpdateWorkspace({ name: wsName, timezone: wsTz });
setWsSaved(true);
setTimeout(() => setWsSaved(false), 2000);
};
const wsDirty = workspace && (wsName !== workspace.name || wsTz !== workspace.timezone);
return (
<>
<h3 className="settings__h">Switch user</h3>
<p className="settings__sub">Test other accounts without signing out.</p>
<div className="settings__users">
{dbUsers.map(u => (
<button key={u.id} className={"login__user" + (u.id===user.id?" is-picked":"")} onClick={() => onSwitchUser(u.id)}>
<Avatar user={u} size={36} />
<div className="login__user-meta">
<span className="login__user-name">
{u.name}
{u.account_type === 'admin' && <span className="admin-badge"></span>}
</span>
<span className="login__user-role">{u.account_type === 'admin' ? 'Admin · ' : ''}{u.role}</span>
</div>
</button>
))}
</div>
<div className="settings__divider" />
<div className="settings__h-row">
<h3 className="settings__h">Team members {!isAdmin && <span className="mono lock"> admin only</span>}</h3>
{isAdmin && !adding && (
<button className="btn btn--soft btn--sm" onClick={() => setAdding(true)}>+ Add user</button>
)}
</div>
{isAdmin && adding && (
<div className="member-form">
<div className="settings__grid">
<label className="field">
<span className="field__label">Name</span>
<input className="field__input" value={newName} onChange={e => setNewName(e.target.value)} autoFocus placeholder="Jamie Smith" />
</label>
<label className="field">
<span className="field__label">Role / title</span>
<input className="field__input" value={newRole} onChange={e => setNewRole(e.target.value)} placeholder="Apprentice" />
</label>
</div>
<div className="field">
<span className="field__label">Account type</span>
<div className="picker">
<button type="button" className={"picker__item"+(newType==='standard'?' is-on':'')} onClick={() => setNewType('standard')}>Standard</button>
<button type="button" className={"picker__item"+(newType==='admin'?' is-on':'')} onClick={() => setNewType('admin')}>Admin</button>
</div>
<span className="settings__hint mono">
{newType === 'admin'
? 'Can see audit log, manage users, edit roles & workspace'
: 'Can manage own tasks; cannot see logs or DB'}
</span>
</div>
<div className="settings__save-row">
<button className="btn btn--ghost" onClick={() => setAdding(false)}>Cancel</button>
<button className="btn btn--primary" onClick={submit} disabled={!newName.trim()}>Add user</button>
</div>
</div>
)}
<ul className="member-list">
{dbUsers.map(u => (
<li key={u.id} className="member-row">
<Avatar user={u} size={32} />
<div className="member-row__meta" style={{ flex: 1 }}>
{editingUserId === u.id ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
<input className="field__input field__input--sm" value={editName} onChange={e => setEditName(e.target.value)} autoFocus />
<input className="field__input field__input--sm" value={editRole} onChange={e => setEditRole(e.target.value)} />
</div>
) : (
<>
<div className="member-row__name" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
{u.name}
{isAdmin && (
<button className="btn btn--ghost btn--sm" style={{ padding: '2px', height: 'auto', minWidth: 0, opacity: 0.5 }} onClick={() => startEditing(u)}>
<svg viewBox="0 0 16 16" width="10" height="10" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M12.5 3.5l-8 8V14h2.5l8-8-2.5-2.5z" strokeLinecap="round" strokeLinejoin="round"/><path d="M10.5 5.5l2 2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</button>
)}
</div>
<div className="member-row__role mono">{u.role} · {u.email || (u.id + '@murchison-auto.co')}</div>
</>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
{editingUserId === u.id ? (
<div style={{ display: 'flex', gap: '0.25rem' }}>
<button className="btn btn--ghost btn--sm" onClick={cancelEditing}>Cancel</button>
<button className="btn btn--primary btn--sm" onClick={() => saveUserEdit(u.id)}>Save</button>
</div>
) : (
<>
{isAdmin ? (
<div className="picker member-row__type">
<button className={"picker__item"+(u.account_type==='standard'?' is-on':'')}
onClick={() => onUpdateUserRole(u.id, { account_type: 'standard' })}>Standard</button>
<button className={"picker__item"+(u.account_type==='admin'?' is-on':'')}
onClick={() => onUpdateUserRole(u.id, { account_type: 'admin' })}>Admin</button>
</div>
) : (
<span className="chip">{u.account_type}</span>
)}
{isAdmin && u.id !== user.id && (
<button className="member-row__del" onClick={() => {
if (confirm('Remove ' + u.name + '? Their tasks will move to ROD.')) onDeleteUser(u.id);
}} title="Remove user">×</button>
)}
</>
)}
</div>
</li>
))}
</ul>
<div className="settings__divider" />
<h3 className="settings__h">Workspace {!isAdmin && <span className="mono lock"> admin only</span>}</h3>
<div className="settings__grid">
<label className="field">
<span className="field__label">Workspace name</span>
<input className="field__input" value={wsName} onChange={e => setWsName(e.target.value)} disabled={!isAdmin} />
</label>
<label className="field">
<span className="field__label">Timezone</span>
<input className="field__input" value={wsTz} onChange={e => setWsTz(e.target.value)} disabled={!isAdmin} />
</label>
</div>
{isAdmin && (
<div className="settings__save-row" style={{ marginTop: '1rem' }}>
{wsSaved && <span className="settings__saved mono"><Icon.Check /> Saved</span>}
<button className="btn btn--ghost" onClick={() => { setWsName(workspace.name); setWsTz(workspace.timezone); }} disabled={!wsDirty}>Discard</button>
<button className="btn btn--primary" onClick={handleUpdateWorkspace} disabled={!wsDirty}>Update workspace</button>
</div>
)}
</>
);
}
function DeletedScreen({ tasks, onRestore }) {
return (
<div className="audit">
<header className="audit__head">
<div>
<h1 className="audit__title">Trash</h1>
<p className="audit__sub">Recently deleted tasks · <span className="mono">Admins only</span></p>
</div>
</header>
<div className="audit__list">
{tasks.length === 0 ? (
<div className="column__empty" style={{ marginTop: '4rem' }}>
<span className="mono"> trash is empty </span>
</div>
) : (
<div className="user-view__grid" style={{ padding: '0 2rem' }}>
{tasks.map(t => (
<div key={t.id} style={{ position: 'relative' }}>
<TaskCard task={t} />
<button
className="btn btn--primary btn--sm"
style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 10 }}
onClick={() => onRestore(t.id)}
>
Restore
</button>
</div>
))}
</div>
)}
</div>
</div>
);
}
Object.assign(window, {
LoginScreen, TopBar, OverviewScreen, UserScreen, AddTaskModal, Modal, TaskDetail, AuditScreen, HeadsUp, BrandMark,
SettingsScreen,
DeletedScreen,
});