›
byrcsc/laravel-assignment · 1.x
Assign an enquiry to a tradie, rotate enquiries fairly across a team, then offer work the assignee can refuse.
This walkthrough routes enquiries to tradies three ways: by naming the assignee, by letting a policy pick one, and by offering the work so the tradie can decline it. It assumes Laravel Assignment is installed.
use ByRcsc\LaravelAssignment\Concerns\Assignable;
use ByRcsc\LaravelAssignment\Concerns\Assignee;
use Illuminate\Database\Eloquent\Model;
class Enquiry extends Model
{
use Assignable;
protected $guarded = [];
}
class Tradie extends Model
{
use Assignee;
protected $guarded = [];
}The enquiries table needs a trade, and the tradies table needs an
available flag. Nothing else is required, because assignment rows live in the
package's own table.
$enquiry = Enquiry::query()->create(['trade' => 'plumbing']);
$ada = Tradie::query()->create(['available' => true]);
$bo = Tradie::query()->create(['available' => true]);
$enquiry->assign($ada);
$enquiry->assignee(); // the Tradie Ada
$enquiry->isAssigned(); // true
$enquiry->reassign($bo); // ends Ada's row, opens Bo's
$enquiry->unassign(); // ends Bo's row
$enquiry->assignments()->count(); // 2, the full history
$enquiry->isAssigned(); // falseNothing was deleted. Both rows are still there. Each has an ended_at, and
their ended_reason values are reassigned and unassigned.
Calling assign() on a slot that already holds an open row throws
SlotOccupied. Reassignment is a separate verb because replacing somebody is a
different decision from filling an empty slot.
Naming each assignee by hand does not work for a team. Give the builder a candidate list and a policy instead:
use ByRcsc\LaravelAssignment\Facades\Assignment;
use ByRcsc\LaravelAssignment\Policies\RoundRobin;
$assignment = Assignment::for($enquiry)
->among(Tradie::query()->where('available', true)->get())
->scope('trade:'.$enquiry->trade)
->using(RoundRobin::class)
->assign();
$assignment->assignee; // whichever tradie was next in this tradeYou bring the candidates. The policy picks one of them.
The scope() string names the rotation cursor. Every plumbing
enquiry shares one cursor, so the plumbers take turns even though you rebuild
the candidate list on every call. Electrical rotates on a cursor of its own.
Run the same block for six enquiries across two trades, and each trade rotates through its own tradies in order. Rebuilding, reordering, growing, or shrinking the candidate list between calls does not break the rotation.
assign() returns null when the candidate list is empty or the policy picks
nobody. It does not throw for that case.
Use offer() when the assignee gets a say:
use ByRcsc\LaravelAssignment\Policies\LeastWorkload;
$offer = Assignment::for($enquiry)
->among(Tradie::query()->where('available', true)->get())
->using(LeastWorkload::class)
->expiresIn(120)
->offer();
$offer->status; // AssignmentStatus::Offered
$offer->expires_at; // two minutes from nowThe offer occupies the slot while it is open. You cannot assign or offer that slot to anyone else until the tradie answers or the two minutes pass.
Only the tradie the offer names can answer it:
$tradie = $offer->assignee;
$tradie->accept($offer); // offered becomes active
// or
$tradie->decline($offer); // the row ends with reason declinedAnother tradie calling accept() on that row gets NotTheAssignee.
$enquiry->assignment(); // the active assignment, or null
$enquiry->openAssignment(); // the active row, or the offer holding the slot
$enquiry->assignee(); // the assignee model, or null
$enquiry->assignments()->ended()->get(); // past holders, oldest first
$tradie->activeAssignments; // what this tradie is holding
$tradie->openAssignments; // active plus unanswered offers
$tradie->workload(); // how many of those there areworkload() counts active rows plus unanswered offers, so one idle tradie
cannot collect ten pending offers. LeastWorkload counts the same way.
$enquiry->autoAssign() takes no arguments.