// ============ TaskQuest — System Admin panel ============ const { useState: useStateA } = React; const { ROLES: RAd, PROJECTS: PAd } = window.TQ; function AdminCard({ icon, iconColor, title, sub, children }) { return (
{React.createElement(icon, { size: 16 })}
{title}
{sub &&
{sub}
}
{children}
); } function ProjectPicker({ value, onChange }) { const projects = Object.values(window.TQ.PROJECTS); const sel = Array.isArray(value) ? value : []; function toggle(k) { onChange(sel.includes(k) ? sel.filter(x => x !== k) : [...sel, k]); } if (projects.length === 0) { return
No projects yet — add one in the Projects card first.
; } return (
{projects.map(p => { const on = sel.includes(p.key); return ( ); })}
); } function UserRow({ u, onEdit }) { const r = window.TQ.ROLES[u]; if (!r) return null; const projs = window.TQ.projectsOf(r); return (
{r.name} {r.username && @{r.username}} {projs.length > 0 && ( {projs.slice(0, 2).map(k => ( {window.TQ.PROJECTS[k].name} ))} {projs.length > 2 && +{projs.length - 2}} )} {r.label}
); } function EditUserForm({ id, live, isSelf, onSave, onDelete, onCancel }) { const r = window.TQ.ROLES[id] || {}; const [name, setName] = useStateA(r.name || ""); const [uname, setUname] = useStateA(r.username || ""); const [type, setType] = useStateA(r.type || "consultant"); const [projs, setProjs] = useStateA(window.TQ.projectsOf(r)); const [pw, setPw] = useStateA(""); const [show, setShow] = useStateA(false); const [busy, setBusy] = useStateA(false); const [msg, setMsg] = useStateA(null); const [confirmDel, setConfirmDel] = useStateA(false); const pwOk = pw === "" || pw.length >= 6; const valid = name.trim().length > 1 && pwOk; async function save() { if (!valid || busy) return; setBusy(true); setMsg(null); const res = await onSave({ id, name: name.trim(), username: uname.trim() || null, type, projects: projs, password: pw || null }); setBusy(false); if (res && res.ok) { onCancel(); } else { setMsg({ ok: false, text: (res && res.message) || "Could not update the profile." }); } } async function remove() { if (busy) return; setBusy(true); setMsg(null); const res = await onDelete(id); setBusy(false); if (res && res.ok) { onCancel(); } else { setConfirmDel(false); setMsg({ ok: false, text: (res && res.message) || "Could not remove the user." }); } } return (
Edit profile
{ setName(e.target.value); setMsg(null); }} />
setUname(e.target.value)} />
{ setPw(e.target.value); setMsg(null); }} />
{!pwOk &&
Password must be at least 6 characters.
}
{msg && (
{msg.text}
)}
{/* Danger zone — remove the user entirely */}
{!confirmDel ? ( ) : (
Permanently delete {r.name}? Their login is removed and their tasks are left unassigned. This can't be undone.
)} {isSelf &&
You can't delete the account you're signed in with.
}
{!live &&
Preview mode — changes are kept locally only.
}
); } function UsersAdmin({ onCreate, onUpdate, onDelete, meId }) { const live = window.TQ_BACKEND.CONFIGURED; const [name, setName] = useStateA(""); const [email, setEmail] = useStateA(""); const [uname, setUname] = useStateA(""); const [pw, setPw] = useStateA(""); const [type, setType] = useStateA("consultant"); const [projs, setProjs] = useStateA([]); const [show, setShow] = useStateA(false); const [busy, setBusy] = useStateA(false); const [msg, setMsg] = useStateA(null); // { ok, text } const [editId, setEditId] = useStateA(null); const users = [...window.TQ.CONSULTANTS, ...window.TQ.MANAGERS, ...Object.keys(window.TQ.ROLES).filter(k => window.TQ.ROLES[k].type === "admin")]; const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); const valid = name.trim().length > 1 && emailOk && pw.length >= 6; async function submit() { if (!valid || busy) return; setBusy(true); setMsg(null); const res = await onCreate({ name: name.trim(), email: email.trim(), username: uname.trim() || null, password: pw, type, projects: projs }); setBusy(false); if (res && res.ok) { setMsg({ ok: true, text: `Login created. Share the email + password with ${name.trim()}.` }); setName(""); setEmail(""); setUname(""); setPw(""); setType("consultant"); setProjs([]); } else { setMsg({ ok: false, text: (res && res.message) || "Could not create the account." }); } } return (
{users.length === 0 &&
No members yet — provision the first account below.
} {users.map(u => ( editId === u ? setEditId(null)} /> : { setEditId(id); setMsg(null); }} /> ))}
Provision a login
{ setName(e.target.value); setMsg(null); }} />
{ setEmail(e.target.value); setMsg(null); }} />
{ setUname(e.target.value); setMsg(null); }} />
{ setProjs(p); setMsg(null); }} />
{ setPw(e.target.value); setMsg(null); }} />
{msg && (
{React.createElement(msg.ok ? Ic.CheckCircle : Ic.Alert, { size: 14 })} {msg.text}
)} {!live &&
Preview mode — the account is created locally and not saved.
}
); } function ProjectEditForm({ pk, onSave, onDelete, onCancel }) { const p = window.TQ.PROJECTS[pk] || {}; const [name, setName] = useStateA(p.name || ""); const [color, setColor] = useStateA(p.color || window.TQ.PALETTE[0]); const [busy, setBusy] = useStateA(false); const [msg, setMsg] = useStateA(null); const [confirmDel, setConfirmDel] = useStateA(false); const valid = name.trim().length > 1; async function save() { if (!valid || busy) return; setBusy(true); setMsg(null); const res = await onSave(pk, { name: name.trim(), color }); setBusy(false); if (res && res.ok) onCancel(); else setMsg((res && res.message) || "Could not update the project."); } async function remove() { if (busy) return; setBusy(true); setMsg(null); const res = await onDelete(pk); setBusy(false); if (res && res.ok) onCancel(); else { setConfirmDel(false); setMsg((res && res.message) || "Could not remove the project."); } } return (
Edit project
{ setName(e.target.value); setMsg(null); }} onKeyDown={e => { if (e.key === "Enter") save(); }} />
{window.TQ.PALETTE.map(c => (
{msg &&
{msg}
}
{!confirmDel ? ( ) : (
Delete {p.name}? Tasks in this project are kept but left without a project. This can't be undone.
)}
); } function ProjectsAdmin({ onCreate, onCreateMany, onEdit, onDelete }) { const [mode, setMode] = useStateA("single"); // 'single' | 'bulk' const [name, setName] = useStateA(""); const [color, setColor] = useStateA(window.TQ.PALETTE[0]); const [bulk, setBulk] = useStateA(""); const [busy, setBusy] = useStateA(false); const [editKey, setEditKey] = useStateA(null); const projects = Object.values(window.TQ.PROJECTS); const valid = name.trim().length > 1; // parse one-project-per-line (also tolerates commas), de-duped, trimmed const bulkNames = Array.from(new Set( bulk.split(/[\n,]+/).map(s => s.trim()).filter(s => s.length > 1) )); function nextColor(usedExtra) { const used = Object.values(window.TQ.PROJECTS).map(p => p.color).concat(usedExtra || []); return window.TQ.PALETTE.find(c => !used.includes(c)) || window.TQ.PALETTE[(used.length) % window.TQ.PALETTE.length]; } function submitSingle() { if (!valid) return; onCreate({ name: name.trim(), color }); setName(""); setColor(nextColor()); } async function submitBulk() { if (!bulkNames.length || busy) return; setBusy(true); const used = []; const list = bulkNames.map(nm => { const c = nextColor(used); used.push(c); return { name: nm, color: c }; }); const res = await onCreateMany(list); setBusy(false); if (!res || res.ok) setBulk(""); } return (
{projects.length === 0 &&
No projects yet — add one below.
} {projects.map(p => ( editKey === p.key ? setEditKey(null)} /> : (
{p.name} {p.key}
) ))}
{mode === "single" ? ( <> setName(e.target.value)} onKeyDown={e => { if (e.key === "Enter") submitSingle(); }} />
{window.TQ.PALETTE.map(c => (
) : ( <>