From b62bf6fb34c3db1dec3b05652606f0f0bd581e0b Mon Sep 17 00:00:00 2001 From: red Date: Fri, 5 Dec 2025 11:40:09 +0000 Subject: [PATCH] fix matching if somebody withdraws and dont rematch everybody! --- app/Models/Participant.php | 69 ++++++++--- app/Services/MatcherService.php | 111 ++++++------------ ...25_12_04_105253_add_participants_table.php | 1 + .../views/livewire/pages/profile.blade.php | 10 +- .../views/livewire/pages/table.blade.php | 4 +- .../views/livewire/pages/withdraw.blade.php | 6 +- 6 files changed, 101 insertions(+), 100 deletions(-) diff --git a/app/Models/Participant.php b/app/Models/Participant.php index c31f1f3..d47603b 100644 --- a/app/Models/Participant.php +++ b/app/Models/Participant.php @@ -2,15 +2,18 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Log; class Participant extends Model { use HasFactory; + use SoftDeletes; protected $fillable = [ 'user_id', @@ -20,18 +23,6 @@ class Participant extends Model '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 { return self::where('token', $token)->first(); @@ -69,16 +60,60 @@ class Participant extends Model ->whereDoesntHave('receiver'); } - public function getDesperateAttribute(): bool + public function receiver(): BelongsTo { - $gives = !is_null($this->giver); - $gets = !is_null($this->receiver); + return $this->belongsTo(Participant::class, 'giving_id'); + } - 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 { - 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; } } diff --git a/app/Services/MatcherService.php b/app/Services/MatcherService.php index 9ebd6f9..dbb585f 100644 --- a/app/Services/MatcherService.php +++ b/app/Services/MatcherService.php @@ -14,92 +14,57 @@ class MatcherService 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 = []; - /* - |-------------------------------------------------------------------------- - | 1. Match DESPERATE participants - |-------------------------------------------------------------------------- - | A desperate user should match: - | - First to another desperate participant - | - If none left, match to a non-desperate participant - |-------------------------------------------------------------------------- - */ + $newParticipantIds = $participants + ->filter(fn($p) => $p->is_unmatched) + ->pluck('id') + ->shuffle() + ->all(); - foreach ($desperate as $p) { - // Exclude self from possible options - $pool = $desperate->filter(fn($d) => $d->id !== $p->id); + $desperateGiverIds = $participants + ->filter(fn($p) => $p->is_desperate && !$p->hasReceiver()) + ->pluck('id') + ->shuffle() + ->all(); - if ($pool->isEmpty()) { - $pool = $nonDesperate; - } + $desperateReceiverIds = $participants + ->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; } - $candidate = $this->pickValidTarget($p, $pool, $assignments); - if ($candidate) { - $assignments[$p->id] = $candidate->id; + $assignments[$giverId] = $receiverId; + } + + $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) { - // Exclude self - $pool = $nonDesperate->filter(fn($d) => $d->id !== $p->id); - - 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, + foreach ($assignments as $giverId => $receiverId) { + Participant::where('id', $giverId)->update([ + 'giving_id' => $receiverId, ]); } } - - 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; - } } diff --git a/database/migrations/2025_12_04_105253_add_participants_table.php b/database/migrations/2025_12_04_105253_add_participants_table.php index eaedf3e..3013bad 100644 --- a/database/migrations/2025_12_04_105253_add_participants_table.php +++ b/database/migrations/2025_12_04_105253_add_participants_table.php @@ -24,6 +24,7 @@ return new class extends Migration $table->string('token'); $table->timestamps(); + $table->softDeletes(); }); } diff --git a/resources/views/livewire/pages/profile.blade.php b/resources/views/livewire/pages/profile.blade.php index 8fb564b..ce8e786 100644 --- a/resources/views/livewire/pages/profile.blade.php +++ b/resources/views/livewire/pages/profile.blade.php @@ -27,7 +27,7 @@ rules([ ], ]); -mount(function ($token): null +mount(function ($token) { $this->token = $token; if ($this->token) { @@ -51,7 +51,7 @@ mount(function ($token): null } }); -$saveEditingPrompt = function (): null +$saveEditingPrompt = function () { if ($this->participant) { $this->validate(); @@ -62,13 +62,13 @@ $saveEditingPrompt = function (): null } }; -$cancelEditingPrompt = function (): null +$cancelEditingPrompt = function () { $this->prompt = $this->participant->prompt ?? ''; $this->isEditingPrompt = false; }; -$saveEditingSubmission = function (): null +$saveEditingSubmission = function () { if ($this->participant) { $this->validate(); @@ -79,7 +79,7 @@ $saveEditingSubmission = function (): null } }; -$cancelEditingSubmission = function (): null +$cancelEditingSubmission = function () { $this->submissionUrl = $this->participant->submission_url ?? ''; $this->isEditingSubmission = false; diff --git a/resources/views/livewire/pages/table.blade.php b/resources/views/livewire/pages/table.blade.php index 0452b0a..b921f0e 100644 --- a/resources/views/livewire/pages/table.blade.php +++ b/resources/views/livewire/pages/table.blade.php @@ -88,8 +88,8 @@ $runMatch = function () { - {{ $participant->desperate ? 'YES' : 'No' }} + {{ $participant->is_desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}"> + {{ $participant->is_desperate ? 'YES' : 'No' }} diff --git a/resources/views/livewire/pages/withdraw.blade.php b/resources/views/livewire/pages/withdraw.blade.php index cce8eff..2ad1e09 100644 --- a/resources/views/livewire/pages/withdraw.blade.php +++ b/resources/views/livewire/pages/withdraw.blade.php @@ -12,7 +12,7 @@ state([ 'participant' => null, ]); -mount(function ($token): null +mount(function ($token) { $this->token = $token; if ($this->token) { @@ -20,7 +20,7 @@ mount(function ($token): null } }); -$confirmWithdrawal = function (): null +$confirmWithdrawal = function () { $this->js(<<<'JS' 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); }; -$withdraw = function (): mixed +$withdraw = function () { if($this->participant->withdraw()) { $this->js('alert("Withdrawal complete")');