// ============ TaskQuest — Deliverables (DM backlog + PM intake) ============
const { useState: useStateD, useEffect: useEffectD } = React;
// ---------- shared chips ----------
function PrioChip({ priority }) {
const p = window.TQ.PRIORITY[priority] || window.TQ.PRIORITY.med;
return (
{p.name}
);
}
function DlvStatusBadge({ status }) {
const s = window.TQ.DLV_STATUS[status];
return (
{s.name}
);
}
function DlvProgress({ comp }) {
return (
);
}
// ---------- DM dashboard: KPIs + primary Backlog / Scoreboard toggle ----------
function DmDashboard({ project, deliverables, tasks, dmLayout, bonus, onOpen, tab = "backlog" }) {
const { computeDeliverable, computeScores, leaderboard, PROJECTS } = window.TQ;
const scoped = project === "all" ? deliverables : deliverables.filter(d => d.project === project);
const comps = scoped.map(d => computeDeliverable(d, tasks));
const total = scoped.length;
const inprog = comps.filter(c => c.status === "inprogress").length;
const awaiting = comps.filter(c => c.status === "delivered").length;
const accepted = comps.filter(c => c.status === "accepted").length;
const atRisk = comps.filter(c => c.overdue && c.status !== "accepted").length;
// team performance + points across the same project scope
// performance = completed ÷ assigned (Happy Path + Needs Attention + Done; On Hold excluded)
const scopedTasks = project === "all" ? tasks : tasks.filter(t => t.project === project);
const stats = Object.values(computeScores(scopedTasks, []));
const completedAll = stats.reduce((a, s) => a + s.completed, 0);
const assignedAll = stats.reduce((a, s) => a + s.assigned, 0);
const teamPerf = assignedAll ? Math.round((completedAll / assignedAll) * 100) : 0;
const board = leaderboard(tasks, bonus || [], project);
const totalPts = board.reduce((a, s) => a + s.total, 0);
const teamAvg = completedAll ? totalPts / completedAll : 0;
const scopeName = project === "all" ? "All projects" : (PROJECTS[project] ? PROJECTS[project].name : "");
return (
<>
{tab === "backlog" ? (
) : (
<>
{total}
{inprog}
{awaiting}
{atRisk}
{board.length > 0 ? (
) : (
No scores yet
Performance and points appear here once consultants start completing tasks.
)}
>
)}
>
);
}
// ---------- Team scoreboard: Performance / Points toggle ----------
function TeamScoreboard({ board, teamPerf, totalPts, teamAvg, completedAll, assignedAll, scopeName, project }) {
const [metric, setMetric] = useStateD(() => localStorage.getItem("tq_dm_metric") || "perf");
useEffectD(() => { try { localStorage.setItem("tq_dm_metric", metric); } catch (e) {} }, [metric]);
const toggle = (
);
const headline = (
{metric === "perf" ? (
<>
{teamPerf}%
Team performance · {completedAll} of {assignedAll} tasks completed
>
) : (
<>
{(teamAvg || 0).toFixed(1)}pts
Avg points / task · {totalPts} pts over {completedAll} completed · {scopeName}
>
)}
);
return (
);
}
// ---------- card / table / board renderers ----------
function DeliverableCard({ d, comp, onOpen }) {
const { fmtDate, PROJECTS, ROLES } = window.TQ;
const proj = PROJECTS[d.project];
const pm = ROLES[d.pm];
return (
onOpen(d)}>
{d.name}
{d.desc &&
{d.desc}
}
{pm ? pm.short : "—"}
{proj &&
{proj.name}}
{fmtDate(d.due)}
);
}
function DeliverableTable({ items, onOpen }) {
const { PROJECTS, ROLES } = window.TQ;
return (
Deliverable
Owner · PM
Priority
Progress
Status
{items.map(({ d, comp }) => {
const proj = PROJECTS[d.project];
return (
onOpen(d)}>
{d.name}
{d.id}{proj ? " · " + proj.name : ""}
{ROLES[d.pm] ? ROLES[d.pm].short : "—"}
);
})}
);
}
function DeliverableBoard({ items, onOpen }) {
const { DLV_STATUS, DLV_STATUS_ORDER, fmtDate } = window.TQ;
const byStatus = {};
DLV_STATUS_ORDER.forEach(k => { byStatus[k] = []; });
items.forEach(it => byStatus[it.comp.status].push(it));
return (
{DLV_STATUS_ORDER.map(k => {
const col = DLV_STATUS[k];
const list = byStatus[k];
return (
{col.name}
{list.length}
{list.length === 0 &&
None
}
{list.map(({ d, comp }) => (
))}
);
})}
);
}
const PRIO_RANK = { high: 0, med: 1, low: 2 };
function sortItems(items) {
return items.slice().sort((a, b) =>
(Number(b.comp.overdue) - Number(a.comp.overdue)) ||
(PRIO_RANK[a.d.priority] - PRIO_RANK[b.d.priority]) ||
String(a.d.due).localeCompare(String(b.d.due)));
}
// ---------- DM page ----------
function DeliverablesPage({ project, deliverables, tasks, dmLayout, onOpen }) {
const { computeDeliverable } = window.TQ;
const [view, setView] = useStateD(() => localStorage.getItem("tq_dlv_view") || "list");
useEffectD(() => { try { localStorage.setItem("tq_dlv_view", view); } catch (e) {} }, [view]);
const scoped = project === "all" ? deliverables : deliverables.filter(d => d.project === project);
const items = sortItems(scoped.map(d => ({ d, comp: computeDeliverable(d, tasks) })));
return (
Delivery backlog
{items.length}
{items.length === 0 ? (
No deliverables yet
Create a deliverable and assign it to a Project Manager to get started.
) : view === "board" ? (
) : dmLayout === "table" ? (
) : (
{items.map(({ d, comp }) => )}
)}
);
}
// ---------- PM intake strip ----------
function PmDeliverablesStrip({ role, project, deliverables, tasks, onOpen, onAddTask }) {
const { computeDeliverable, fmtDate } = window.TQ;
let mine = deliverables.filter(d => d.pm === role);
if (project !== "all") mine = mine.filter(d => d.project === project);
const items = sortItems(mine.map(d => ({ d, comp: computeDeliverable(d, tasks) })));
return (
Deliverables assigned to you
{items.length}
{items.length === 0 ? (
No deliverables assigned to you in this project yet. Your Delivery Manager will add them to the backlog.
) : (
{items.map(({ d, comp }) => (
{fmtDate(d.due)}
{comp.status !== "accepted" && (
)}
))}
)}
);
}
// ---------- deliverable detail ----------
function DeliverableDetailModal({ d, role, tasks, onClose, onComment, onAccept, onAddTask, onOpenTask }) {
const { fmtDate, relDue, daysUntil, computeDeliverable, COLUMNS, ROLES, PROJECTS } = window.TQ;
const r = ROLES[role];
const comp = computeDeliverable(d, tasks);
const pm = ROLES[d.pm];
const proj = PROJECTS[d.project];
const isDM = r.type === "dm";
const isOwnerPM = r.type === "pm" && d.pm === role;
const overdue = comp.overdue;
const [text, setText] = useStateD("");
function submit() { if (!text.trim()) return; onComment(d, text.trim()); setText(""); }
return (
{d.id}·{proj ? proj.name : "—"}}
onClose={onClose}
footer={
<>
{isOwnerPM && comp.status !== "accepted" && (
)}
{isDM && comp.status === "delivered" && (
)}
>
}>
{comp.status === "delivered" && (
All tasks complete
{isDM ? "Review the breakdown and accept this delivery to close it out." : "Delivered — awaiting sign-off from Delivery."}
)}
{comp.status === "accepted" && (
Accepted by Delivery
This deliverable is signed off and closed.
)}
{overdue && comp.status !== "accepted" && comp.status !== "delivered" && (
At risk
One or more tasks are overdue — see the breakdown below.
)}
Target date
{fmtDate(d.due)} · {relDue(d.due)}
{d.desc && {d.desc}
}
Breakdown · {comp.done}/{comp.total} tasks done
{comp.total === 0 ? (
{isOwnerPM ? "Not broken down yet — add the first task for your team." : "The PM hasn't broken this down into tasks yet."}
) : comp.kids.map(t => {
const col = COLUMNS.find(c => c.key === t.status);
const od = t.status !== "done" && daysUntil(t.due) < 0;
return (
onOpenTask && onOpenTask(t)}>{t.name}
{od ? relDue(t.due) : fmtDate(t.due)}
{col.name}
);
})}
Updates
{(!d.comments || d.comments.length === 0) &&
No updates yet.
}
{d.comments && d.comments.map((c, i) => (
{ROLES[c.author] ? ROLES[c.author].name : "—"}
{ROLES[c.author] ? ROLES[c.author].label : ""}
{c.time}
{c.text}
))}
);
}
// ---------- create deliverable (DM) ----------
function CreateDeliverableModal({ onClose, onCreate }) {
const { ROLES, PROJECTS, PRIORITY, projectsOf, isoFromOffset } = window.TQ;
const pms = Object.values(ROLES).filter(x => x.type === "pm");
const [name, setName] = useStateD("");
const [pm, setPm] = useStateD(pms[0] ? pms[0].key : "");
const [priority, setPriority] = useStateD("med");
const [due, setDue] = useStateD(isoFromOffset(21));
const [desc, setDesc] = useStateD("");
const pmProjects = pm && ROLES[pm] ? projectsOf(ROLES[pm]) : [];
const [proj, setProj] = useStateD(pmProjects[0] || Object.keys(PROJECTS)[0] || "");
useEffectD(() => {
const list = pm && ROLES[pm] ? projectsOf(ROLES[pm]) : [];
if (list.length && list.indexOf(proj) < 0) setProj(list[0]);
}, [pm]);
const noPMs = pms.length === 0;
const valid = name.trim() && pm && proj && due;
return (
>}>
setName(e.target.value)} />
{noPMs ? (
No Project Managers yet — ask an admin to add one.
) : (
{pms.map(p => (
setPm(p.key)}>
{p.name}
{projectsOf(p).map(k => PROJECTS[k] ? PROJECTS[k].name : k).join(", ") || "—"}
))}
)}
{pmProjects.length > 1 && (
)}
{pmProjects.length <= 1 && (
)}
{Object.values(PRIORITY).map(p => (
setPriority(p.key)}
style={priority === p.key ? { borderColor: p.color, boxShadow: `inset 0 0 0 1px ${p.color}` } : {}}>
{p.name}
))}
);
}
Object.assign(window, {
DmDashboard, DeliverablesPage, PmDeliverablesStrip,
DeliverableDetailModal, CreateDeliverableModal,
});