// ============ 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 (
{comp.done}/{comp.total}
); } // ---------- 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.id}
{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 }) => (
onOpen(d)}>
{d.name}
{fmtDate(d.due)}
))}
); })}
); } 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 }) => (
onOpen(d)}>{d.name}
{d.id}
{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.
)}
Assigned PM
{pm ? pm.name : "—"}
Target date
{fmtDate(d.due)} · {relDue(d.due)}
Priority
Status
{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}
))}