Messy plan, one click
Four tasks have nobody, Dee carries three tasks at once, Ben has nothing to do in the second week, and Eli is not used at all. Press Auto-assign: the engine proposes who should do what, the chart shows where the bars would go, and the log lists every decision. Nothing changes until you pressApply.
The assign-and-compare demo answers “who should take this one task”: evaluate scores each person for a single task. HeresuggestAssignments walks the whole plan, asks that question task by task, and builds on its own picks: the second task is scored on a plan that already has the first one assigned.
Slots: one per task
The engine does not know about skills or roles. It sees slots: a task, the people who may take it, and the assignment filling it, if any. In this demo every person may take every task, so the slots are built in a few lines of JavaScript. The milestone gets no slot. A new assignment takes the slot's id, so after Apply the next run finds it filled.
// One slot per task. Every person may take every task; a filled slot names its assignment.
const slots = tasks.filter((t) => t.type !== "milestone").map((t) => {
const current = assignments.find((a) => a.task === t.id); // who holds the task now, if anyone
return {
id: `a${t.id}`, // the id a new assignment gets, so the next run binds it
task: t.id,
allowed: ["ana", "ben", "cy", "dee", "eli"], // order = tie-break preference
...(current && { assignment: current.id }),
};
});Options: what “better” means
Fill empty only gives people to tasks nobody has. Rebalance only moves tasks that already have someone, and only when the move is strictly better, so the current person wins ties. Both fills first, then rebalances everything, including its own fills.
Every candidate is scored the way evaluate scores it: a cost of days late plus resource-days over capacity, on the plan after leveling. ThePrefer slider adds a price for each person the plan did not use before: at 5, a new person is worth five days of overload. At 0 the engine spreads work to Eli; at 10 it keeps the team as it is and lets leveling push Dee's tasks later instead. Releasing the slider asks the engine again.
const proposal = project.suggestAssignments(slots, {
targets: ["empty", "valid"], // Both: fill empty slots, then rebalance every filled one
newResourcePenalty: 0, // the Prefer slider: what one more person costs, in days
level: { allowSplits: true }, // rank every candidate on the leveled copy
clearFirst: true, // score from the plan without the last leveling's delays
});The step log
The proposal comes with the decisions that changed something, in visit order: pass, task, from whom to whom, and the plan's cost before and after. Tasks are visited by start date, fixed once from the plan as it is, and each pick is kept for the next. That makes the result greedy: an early pick can leave a later one with no good option. The engine's passes option lets the rebalance revisit them; the demo runs the default single pass.
// Only the decisions that changed something, in visit order.
proposal.steps = [
{ slot: "a10", task: 10, pass: 0, fromResource: null, toResource: "ben", cost: { before: 5, after: 5 } },
{ slot: "a5", task: 5, pass: 1, fromResource: "dee", toResource: "ben", cost: { before: 5, after: 2 } },
{ slot: "a6", task: 6, pass: 1, fromResource: "dee", toResource: "eli", cost: { before: 2, after: 1 } },
…
];
proposal.unfilled; // slots no candidate could be scored for: []
proposal.baseline; // { cost: 5, resources: 4 } the plan now
proposal.proposed; // { cost: 0, resources: 5, scheduled, leveled } after the proposalProposal, then commit
suggestAssignments never changes the project. The demo previews the proposal on a copy with the same calls that Apply makes on the project: apply the changes, then levelwith the options that scored them. The project is solved with gap_behavior: "compress", so the committed plan is the one that was scored. After Apply, click a person on any bar to reassign by hand, then switch to Rebalance and press Auto-assign again.
// Nothing was applied yet. The preview runs the commit on a copy:
const copy = project.clone();
copy.apply(proposal.changes);
const preview = copy.level({ allowSplits: true });
copy.dispose();
// Apply runs the same two calls on the project.
project.apply(proposal.changes); // cleared delays, adds { id, task, resource, value }, updates { id, resource }
project.level({ allowSplits: true });