// ============ TaskQuest — Modals ============
const { useState, useEffect, useRef } = React;
const { ROLES: R5, PROJECTS: P5, CONSULTANTS: C5, COLUMNS: COLS5 } = window.TQ;
function ModalShell({ icon, iconColor, title, desc, onClose, children, footer, wide }) {
useEffect(() => {
function esc(e) { if (e.key === "Escape") onClose(); }
document.addEventListener("keydown", esc);
return () => document.removeEventListener("keydown", esc);
}, []);
return (
e.stopPropagation()}>
{icon &&
{React.createElement(icon, { size: 18 })} }
{children}
{footer &&
{footer}
}
);
}
function ProjectTag({ pk }) {
const p = P5[pk];
return {p.name} ;
}
// ---------- Task detail ----------
function TaskDetailModal({ task, role, onClose, onComment, onApproveDate, onRejectDate, onApproveDone, onAwardBonus, onReassign, onDelete, onEdit, onMoveBackHint }) {
const { fmtDateLong, relDue, daysUntil } = window.TQ;
const r = R5[role];
const isManager = r.type === "pm" || r.type === "dm";
const col = COLS5.find(c => c.key === task.status);
const [text, setText] = useState("");
const [editDate, setEditDate] = useState(task.request ? task.request.newDate : task.due);
const [reassignOpen, setReassignOpen] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [editing, setEditing] = useState(false);
// edit form state
const SUBCATS = window.TQ.SUBCATEGORIES;
const [editName, setEditName] = useState(task.name);
const [editDue, setEditDue] = useState(task.due);
const [editAssignee, setEditAssignee] = useState(task.assignee);
const [editSubcat, setEditSubcat] = useState(task.subcategory || "");
const [editDesc, setEditDesc] = useState(task.description || "");
const overdue = task.status !== "done" && daysUntil(task.due) < 0;
const subcat = task.subcategory && SUBCATS.find(s => s.key === task.subcategory);
const canReassign = isManager && (task.status === "happy" || task.status === "unhappy");
const reassignPool = C5.filter(c => R5[c] && window.TQ.projectsOf(R5[c]).indexOf(task.project) >= 0);
const pool = reassignPool.length ? reassignPool : C5;
const datePending = task.request && task.request.status === "pending";
const donePending = task.completion && task.completion.status === "pending";
function submitComment() {
if (!text.trim()) return;
onComment(task, text.trim());
setText("");
}
return (
{task.id}· }
onClose={onClose}
footer={
<>
{r.type === "dm" && (
onAwardBonus(task.assignee, task.project)}>
Award bonus
)}
{isManager && !editing && (
{ setEditing(true); setEditName(task.name); setEditDue(task.due); setEditAssignee(task.assignee); setEditSubcat(task.subcategory||""); setEditDesc(task.description||""); }}>
Edit
)}
{isManager && editing && (
{ onEdit && onEdit(task.id, { name:editName.trim(), due:editDue, assignee:editAssignee, subcategory:editSubcat||null, description:editDesc }); setEditing(false); onClose(); }}> Save changes
setEditing(false)}>Cancel
)}
Close
>
}>
{/* Inline edit form — managers only */}
{isManager && editing && (
Task name
setEditName(e.target.value)} />
Sub-category
{SUBCATS.map(s => (
setEditSubcat(editSubcat === s.key ? "" : s.key)}>
{s.name}
))}
Assign to
(R5[c] && R5[c].name) || c}
renderOption={c => ({R5[c].name} {R5[c].label} )} />
Due date
Description
{!confirmDelete ? (
setConfirmDelete(true)}>
Delete this task
) : (
Are you sure? This cannot be undone.
{ onDelete && onDelete(task.id); onClose(); }}> Yes, delete
setConfirmDelete(false)}>Cancel
)}
)}
{/* Manager approval surfaces */}
{isManager && datePending && (
Date change requested
{R5[task.assignee].name} wants to move the due date from {fmtDateLong(task.due)} to {fmtDateLong(task.request.newDate)} .
Reason: {task.request.reason}
Approve as:
setEditDate(e.target.value)} style={{ maxWidth: 170 }} />
onApproveDate(task, editDate)}> Approve change
onRejectDate(task)}> Reject
)}
{isManager && donePending && (
Completion awaiting sign-off
Marked done {task.completion.onTime ? "on time" : "late"}. {task.completion.comments}
onApproveDone(task)}> Approve completion
)}
{/* Consultant view of their request status */}
{!isManager && task.request && task.request.status === "approved" && (
Date change approved
New due date: {fmtDateLong(task.due)} . Move this task back to Happy Path when you resume work.
)}
{!isManager && task.request && task.request.status === "rejected" && (
Date change rejected
Keep the original due date of {fmtDateLong(task.due)} , or discuss with your manager.
)}
{!isManager && datePending && (
Awaiting approval
Requested new date: {fmtDateLong(task.request.newDate)} . Your manager has been notified.
)}
{/* Details */}
Status
{col.name}
{subcat && (
Sub-category
{subcat.name}
)}
Assignee
{R5[task.assignee].name}
{canReassign && (
setReassignOpen(o => !o)}>{reassignOpen ? "Cancel" : "Reassign"}
)}
{canReassign && reassignOpen && (
Reassign to a different team member
c !== task.assignee)} value="" onChange={(c) => { onReassign && onReassign(task, c); setReassignOpen(false); }}
placeholder="Search team members…" emptyText="No other members on this project"
getLabel={c => (R5[c] && R5[c].name) || c}
renderOption={c => ({R5[c].name} {R5[c].type === "consultant" ? "Consultant" : R5[c].label} )} />
)}
Due date
{fmtDateLong(task.due)} · {relDue(task.due)}
{task.status === "done" && task.completion && (
Outcome
{task.completion.onTime
? On time · +10 pts
: Late · +0 pts }
{task.completion.status === "pending" && awaiting sign-off }
)}
{task.description && (
Description
{task.description}
)}
{/* Per-card time logging — consultant logging against their own task */}
{!isManager && task.assignee === role && window.TaskWeekLogger &&
React.createElement(window.TaskWeekLogger, { task: task, role: role })}
{/* Comments */}
Progress comments {isManager && · visible to managers }
{(!task.comments || task.comments.length === 0) &&
No comments yet.
}
{task.comments && task.comments.map((c, i) => (
{R5[c.author].name}
{R5[c.author].label}
{c.time}
{c.text}
))}
Comment
);
}
// ---------- Move modals (date / hold / done) ----------
function MoveModal({ kind, task, onClose, onSubmit }) {
const { fmtDateLong, isoFromOffset, daysUntil } = window.TQ;
const [newDate, setNewDate] = useState(task.due);
const [reason, setReason] = useState("");
const [comments, setComments] = useState("");
if (kind === "date") {
const valid = newDate && newDate !== task.due;
return (
Cancel onSubmit({ newDate, reason, comments })}> Submit for approval >}>
Current due date
{fmtDateLong(task.due)}
Requested new date *
Reason
setReason(e.target.value)} />
Comments
);
}
if (kind === "hold") {
const valid = reason.trim();
return (
Cancel onSubmit({ reason, comments })}>Submit >}>
Reason *
setReason(e.target.value)}>
Select a reason…
Waiting on client
Blocked by dependency
Awaiting information
Resource constraint
Deprioritized
Comments
);
}
// done
const cfg = window.TQ.CONFIG;
const onTime = daysUntil(task.due) >= 0;
const earn = onTime ? cfg.onTimePoints : cfg.latePoints;
return (
Cancel onSubmit({ comments, onTime })}> Submit completion >}>
{onTime ? <> On-time completion · +{cfg.onTimePoints} points>
: <> Late completion · +{cfg.latePoints} points>}
{onTime ? "Nice work — this lands on or before the due date." : (cfg.latePoints > 0 ? `Past the due date — earns the reduced late rate of +${cfg.latePoints} points.` : "This is past the due date, so it won't earn points. It still counts toward completion.")}
Completion comments
Your manager will be notified to approve the completion.
);
}
// ---------- Create task (PM — breaks a deliverable down for the team) ----------
function CreateTaskModal({ role, project, deliverables = [], presetDeliverable = null, onClose, onCreate }) {
const SUBCATS = window.TQ.SUBCATEGORIES;
const firstProject = Object.keys(P5)[0] || "";
const [name, setName] = useState("");
const [proj, setProj] = useState(() => (project === "all" || !P5[project]) ? firstProject : project);
const scopedCons = C5.filter(c => R5[c] && window.TQ.projectsOf(R5[c]).indexOf(proj) >= 0);
const consultants = scopedCons.length ? scopedCons : C5;
const [assignee, setAssignee] = useState(consultants[0] || "");
const [due, setDue] = useState(window.TQ.isoFromOffset(7));
const [description, setDescription] = useState("");
const [subcategory, setSubcategory] = useState(SUBCATS[0].key);
const [comments, setComments] = useState("");
const [deliv, setDeliv] = useState(presetDeliverable || "");
// keep assignee + linked deliverable valid as the project changes
useEffect(() => {
const list = C5.filter(c => R5[c] && window.TQ.projectsOf(R5[c]).indexOf(proj) >= 0);
const use = list.length ? list : C5;
if (use.indexOf(assignee) < 0) setAssignee(use[0] || "");
if (deliv && !deliverables.some(x => x.id === deliv && x.project === proj)) setDeliv("");
}, [proj]);
const delivOpts = deliverables.filter(x => x.project === proj && !x.accepted);
const validAssignee = !!assignee && !!R5[assignee];
const validProject = !!proj && !!P5[proj];
const valid = name.trim() && validAssignee && validProject && due;
const noConsultants = consultants.length === 0;
return (
Cancel onCreate({ name: name.trim(), assignee, due, description: description.trim(), subcategory, comments, project: proj, deliverable: deliv || null })}> Create task >}>
Task name *
setName(e.target.value)} />
{project === "all" && (
Project
setProj(e.target.value)}>
{Object.values(P5).map(p => {p.name} )}
)}
Sub-category
{SUBCATS.map(s => (
setSubcategory(s.key)}>
{s.name}
))}
{(delivOpts.length > 0 || presetDeliverable) && (
Part of deliverable
setDeliv(e.target.value)} disabled={!!presetDeliverable}>
— None —
{delivOpts.map(x => {x.id} · {x.name} )}
)}
Assign to *
{noConsultants ? (
No consultants on this project yet — ask an admin to add one before creating tasks.
) : (
(R5[c] && R5[c].name) || c}
renderOption={c => (
{R5[c].name}
{R5[c].type === "consultant" ? "Consultant" : R5[c].label}
)} />
)}
Due date *
Description
Comments
);
}
// ---------- Bonus points (DM) ----------
function BonusModal({ toKey, project, onClose, onAward }) {
const tiers = window.TQ.CONFIG.bonusTiers;
const [pts, setPts] = useState(tiers[Math.min(1, tiers.length - 1)]);
const [comment, setComment] = useState("");
const [who, setWho] = useState(toKey || window.TQ.CONSULTANTS[0]);
const valid = comment.trim();
return (
Cancel onAward({ to: who, pts, comment: comment.trim(), project })} style={{ background: "var(--amber)", color: "#3a2e00" }}> Award +{pts} >}>
Recognition comment *
);
}
window.ModalShell = ModalShell;
window.TaskDetailModal = TaskDetailModal;
window.MoveModal = MoveModal;
window.CreateTaskModal = CreateTaskModal;
window.BonusModal = BonusModal;