// ============ 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 })}
{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); }} />
{ 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); }} />
{ setProjs(p); 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 => (
>
) : (
<>
);
}
function PointsAdmin({ onSave }) {
const cfg = window.TQ.CONFIG;
const [ontime, setOntime] = useStateA(cfg.onTimePoints);
const [late, setLate] = useStateA(cfg.latePoints);
const [tiers, setTiers] = useStateA(cfg.bonusTiers.join(", "));
const [saved, setSaved] = useStateA(false);
function save() {
const parsed = tiers.split(",").map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n > 0);
onSave({
onTimePoints: Math.max(0, parseInt(ontime, 10) || 0),
latePoints: Math.max(0, parseInt(late, 10) || 0),
bonusTiers: parsed.length ? parsed.slice(0, 4) : [5, 10, 20],
});
setSaved(true); setTimeout(() => setSaved(false), 1800);
}
return (
setTiers(e.target.value)} placeholder="5, 10, 20" />
Performance score(On-time ÷ Total assigned) × 100
Leaderboard rankTotal points, then performance %
);
}
function AdminPanel({ meId, onCreateUser, onUpdateUser, onDeleteUser, onCreateProject, onCreateProjects, onEditProject, onDeleteProject, onSaveConfig }) {
return (
);
}
window.AdminPanel = AdminPanel;