Who should take this task?
Click the resource name on any bar. A popup lists every possible assignee with what the choice would do to the plan compared with the current assignee: how much later the task and the project would finish, and how much overload leveling had to absorb to get there. Pick one and it is applied.
Reassigning a task is the obvious way to fix an overbooking, and the hard one to judge by eye. The new person may be free this week and buried next week, and moving the task may push a whole chain. The popup does not guess. It asks the engine.
Every candidate on its own copy
evaluate takes a list of change sets and scores each one against the accepted plan. For every candidate the engine copies the plan, applies the change, schedules it and measures the result. Asked to, it also levels the copy and measures again, the same way the plan is leveled when you commit. The project itself is never touched, so opening the popup costs nothing and you can open it as often as you like.
// The popup opens: one candidate per resource, all scored in one call.
const ev = project.evaluate(
resources.map((r) => ({
id: r.id,
changes: { assignments: { update: [{ id: "a5", resource: r.id }] } },
})),
{ level: { allowSplits: true }, focus: 5 }, // level each copy like the commit does, watch task 5
);Numbers to compare, not a verdict
Each candidate comes back with metrics for the plan after the change (scheduled) and for the same copy after leveling (leveled). The popup subtracts the current assignee's finish, so the finish column is a difference in working days after leveling, for the project by default. The overload column is the change in the plan's overload before leveling, in person-days (load above capacity, summed over the days). Click a column caption to switch it to the person's own overload during the task, or to the task's finish. The order is the engine's: every candidate comes with a cost of its leveled copy, the project delay (never below zero) plus the overload left plus 1000 for each conflict or cycle. The list runs from the lowest cost, the current assignee wins a tie, and the best tag goes to the first row. So finishing earlier than today does not lift a candidate above one that keeps the date: both cost the same, and the overload left decides.
// One entry per candidate, in input order. Nothing was applied.
ev.candidates[3] = {
id: "dee",
scheduled: { // the plan right after the change
overload: { total: { value: 4, delta: 4 },
focus: [{ task: 5, resources: [{ resource: "dee", overload: { value: 4, delta: 4 } }] }] },
…
},
leveled: { // the same copy once its overload is leveled
finish: { project: { delta: 4 }, tasks: [{ id: 5, finish: { delta: 10 } }] },
…
},
cost: 4, // of the last stage: max(0, project delta) + overload + 1000 × (conflicts + cycles)
};Open the popup on Content and compare: some people are free that week, others are busy and leveling would have to push their work, and the project finish with it.
Committing is a separate step
Choosing a row applies the change with apply and levels the plan again with the same options that scored the candidates, so the plan you get is the one you were shown. The project is solved with gap_behavior: "compress", and leveling starts from scratch each time, so a task freed from a busy person moves back as early as its links allow instead of keeping an old delay.
// The project is solved compressed and leveled once on load.
project.solve({ config: { gap_behavior: "compress" }, tasks, links, resources, assignments });
project.level({ allowSplits: true });
// Choosing a row commits the change and levels again, with the options that scored it.
project.apply({ assignments: { update: [{ id: "a5", resource: "cy" }] } });
const result = project.level({ allowSplits: true });Delays and pauses from leveling are drawn as in the leveling demo.