fix matching if somebody withdraws and dont rematch everybody!
This commit is contained in:
parent
97aaaf1b85
commit
b62bf6fb34
|
|
@ -2,15 +2,18 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class Participant extends Model
|
class Participant extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
|
|
@ -20,18 +23,6 @@ class Participant extends Model
|
||||||
'token',
|
'token',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $appends = ['desperate'];
|
|
||||||
|
|
||||||
public function receiver() : belongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Participant::class, 'giving_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function giver(): hasOne
|
|
||||||
{
|
|
||||||
return $this->hasOne(Participant::class, 'giving_id', 'id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function findByToken(string $token): ?self
|
public static function findByToken(string $token): ?self
|
||||||
{
|
{
|
||||||
return self::where('token', $token)->first();
|
return self::where('token', $token)->first();
|
||||||
|
|
@ -69,16 +60,60 @@ class Participant extends Model
|
||||||
->whereDoesntHave('receiver');
|
->whereDoesntHave('receiver');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDesperateAttribute(): bool
|
public function receiver(): BelongsTo
|
||||||
{
|
{
|
||||||
$gives = !is_null($this->giver);
|
return $this->belongsTo(Participant::class, 'giving_id');
|
||||||
$gets = !is_null($this->receiver);
|
}
|
||||||
|
|
||||||
return ($gives && !$gets) || (!$gives && $gets);
|
public function giver(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(Participant::class, 'giving_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasReceiver(): bool
|
||||||
|
{
|
||||||
|
return !is_null($this->giving_id); // The simplest and most direct check for an outgoing link
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasGiver(): bool
|
||||||
|
{
|
||||||
|
return $this->hasOne(Participant::class, 'giving_id', 'id')
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isUnmatched(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => !$this->hasReceiver() && !$this->hasGiver(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isDesperate(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->hasReceiver() xor $this->hasGiver(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withdraw(): bool
|
public function withdraw(): bool
|
||||||
{
|
{
|
||||||
return false;
|
if (!$this->id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$giver = Participant::where('giving_id', $this->id)->first();
|
||||||
|
if ($giver) {
|
||||||
|
$giver->giving_id = null;
|
||||||
|
$giver->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userB_id = $this->giving_id;
|
||||||
|
if ($userB_id) {
|
||||||
|
$this->giving_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->delete();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,92 +14,57 @@ class MatcherService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// desperate denotes a user A that is supposed to give to a user B that dropped out
|
|
||||||
// whilst user A has a user C giving to them, or vice versa
|
|
||||||
$desperate = $participants->where('desperate', true)->values();
|
|
||||||
$nonDesperate = $participants->where('desperate', false)->values();
|
|
||||||
|
|
||||||
$desperate = $desperate->shuffle();
|
|
||||||
$nonDesperate = $nonDesperate->shuffle();
|
|
||||||
|
|
||||||
$assignments = [];
|
$assignments = [];
|
||||||
|
|
||||||
/*
|
$newParticipantIds = $participants
|
||||||
|--------------------------------------------------------------------------
|
->filter(fn($p) => $p->is_unmatched)
|
||||||
| 1. Match DESPERATE participants
|
->pluck('id')
|
||||||
|--------------------------------------------------------------------------
|
->shuffle()
|
||||||
| A desperate user should match:
|
->all();
|
||||||
| - First to another desperate participant
|
|
||||||
| - If none left, match to a non-desperate participant
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
foreach ($desperate as $p) {
|
$desperateGiverIds = $participants
|
||||||
// Exclude self from possible options
|
->filter(fn($p) => $p->is_desperate && !$p->hasReceiver())
|
||||||
$pool = $desperate->filter(fn($d) => $d->id !== $p->id);
|
->pluck('id')
|
||||||
|
->shuffle()
|
||||||
|
->all();
|
||||||
|
|
||||||
if ($pool->isEmpty()) {
|
$desperateReceiverIds = $participants
|
||||||
$pool = $nonDesperate;
|
->filter(fn($p) => $p->is_desperate && !$p->hasGiver())
|
||||||
}
|
->pluck('id')
|
||||||
|
->shuffle()
|
||||||
|
->all();
|
||||||
|
|
||||||
if ($pool->isEmpty()) {
|
while (!empty($desperateGiverIds) && !empty($desperateReceiverIds)) {
|
||||||
|
|
||||||
|
$giverId = array_shift($desperateGiverIds);
|
||||||
|
$receiverId = array_shift($desperateReceiverIds);
|
||||||
|
|
||||||
|
if ($giverId === $receiverId) {
|
||||||
|
array_unshift($desperateReceiverIds, $receiverId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$candidate = $this->pickValidTarget($p, $pool, $assignments);
|
$assignments[$giverId] = $receiverId;
|
||||||
if ($candidate) {
|
}
|
||||||
$assignments[$p->id] = $candidate->id;
|
|
||||||
|
$newCount = count($newParticipantIds);
|
||||||
|
if ($newCount >= 2) {
|
||||||
|
|
||||||
|
for ($i = 0; $i < $newCount; $i++) {
|
||||||
|
|
||||||
|
$currentId = $newParticipantIds[$i];
|
||||||
|
|
||||||
|
$nextId = $newParticipantIds[($i + 1) % $newCount];
|
||||||
|
|
||||||
|
$assignments[$currentId] = $nextId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 2. Match NON-DESPERATE participants
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| A non-desperate participant always matches to the non-desperate pool.
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
foreach ($nonDesperate as $p) {
|
foreach ($assignments as $giverId => $receiverId) {
|
||||||
// Exclude self
|
Participant::where('id', $giverId)->update([
|
||||||
$pool = $nonDesperate->filter(fn($d) => $d->id !== $p->id);
|
'giving_id' => $receiverId,
|
||||||
|
|
||||||
if ($pool->isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$candidate = $this->pickValidTarget($p, $pool, $assignments);
|
|
||||||
if ($candidate) {
|
|
||||||
$assignments[$p->id] = $candidate->id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 3. Write all results back to DB
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
foreach ($assignments as $id => $givingId) {
|
|
||||||
Participant::where('id', $id)->update([
|
|
||||||
'giving_id' => $givingId,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function pickValidTarget(Participant $giver, Collection $pool, array $assignments): ?Participant
|
|
||||||
{
|
|
||||||
$shuffled = $pool->shuffle();
|
|
||||||
|
|
||||||
// Avoid duplicates
|
|
||||||
foreach ($shuffled as $candidate) {
|
|
||||||
if (($assignments[$candidate->id] ?? null) === $giver->id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ return new class extends Migration
|
||||||
|
|
||||||
$table->string('token');
|
$table->string('token');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ rules([
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mount(function ($token): null
|
mount(function ($token)
|
||||||
{
|
{
|
||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
if ($this->token) {
|
if ($this->token) {
|
||||||
|
|
@ -51,7 +51,7 @@ mount(function ($token): null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$saveEditingPrompt = function (): null
|
$saveEditingPrompt = function ()
|
||||||
{
|
{
|
||||||
if ($this->participant) {
|
if ($this->participant) {
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
@ -62,13 +62,13 @@ $saveEditingPrompt = function (): null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$cancelEditingPrompt = function (): null
|
$cancelEditingPrompt = function ()
|
||||||
{
|
{
|
||||||
$this->prompt = $this->participant->prompt ?? '';
|
$this->prompt = $this->participant->prompt ?? '';
|
||||||
$this->isEditingPrompt = false;
|
$this->isEditingPrompt = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
$saveEditingSubmission = function (): null
|
$saveEditingSubmission = function ()
|
||||||
{
|
{
|
||||||
if ($this->participant) {
|
if ($this->participant) {
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
@ -79,7 +79,7 @@ $saveEditingSubmission = function (): null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$cancelEditingSubmission = function (): null
|
$cancelEditingSubmission = function ()
|
||||||
{
|
{
|
||||||
$this->submissionUrl = $this->participant->submission_url ?? '';
|
$this->submissionUrl = $this->participant->submission_url ?? '';
|
||||||
$this->isEditingSubmission = false;
|
$this->isEditingSubmission = false;
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,8 @@ $runMatch = function () {
|
||||||
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||||
{{ $participant->desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
|
{{ $participant->is_desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
|
||||||
{{ $participant->desperate ? 'YES' : 'No' }}
|
{{ $participant->is_desperate ? 'YES' : 'No' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ state([
|
||||||
'participant' => null,
|
'participant' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mount(function ($token): null
|
mount(function ($token)
|
||||||
{
|
{
|
||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
if ($this->token) {
|
if ($this->token) {
|
||||||
|
|
@ -20,7 +20,7 @@ mount(function ($token): null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$confirmWithdrawal = function (): null
|
$confirmWithdrawal = function ()
|
||||||
{
|
{
|
||||||
$this->js(<<<'JS'
|
$this->js(<<<'JS'
|
||||||
if (confirm('Are you sure you want to withdraw from the event? This action cannot be undone.')) {
|
if (confirm('Are you sure you want to withdraw from the event? This action cannot be undone.')) {
|
||||||
|
|
@ -29,7 +29,7 @@ $confirmWithdrawal = function (): null
|
||||||
JS);
|
JS);
|
||||||
};
|
};
|
||||||
|
|
||||||
$withdraw = function (): mixed
|
$withdraw = function ()
|
||||||
{
|
{
|
||||||
if($this->participant->withdraw()) {
|
if($this->participant->withdraw()) {
|
||||||
$this->js('alert("Withdrawal complete")');
|
$this->js('alert("Withdrawal complete")');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue