can match

This commit is contained in:
red 2025-12-04 19:00:42 +00:00
parent 8f5aab2c57
commit a930d0f972
4 changed files with 158 additions and 11 deletions

View File

@ -23,12 +23,12 @@ class Participant extends Model
'desperate' => 'boolean',
];
public function giver()
public function receiver()
{
return $this->belongsTo(Participant::class, 'giving_id');
}
public function receiver()
public function giver()
{
return $this->hasOne(Participant::class, 'giving_id', 'id');
}

View File

@ -0,0 +1,105 @@
<?php
namespace App\Services;
use App\Models\Participant;
use Illuminate\Support\Collection;
class MatcherService
{
public function match(): void
{
$participants = Participant::all();
if ($participants->count() < 2) {
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
|--------------------------------------------------------------------------
*/
foreach ($desperate as $p) {
// Exclude self from possible options
$pool = $desperate->filter(fn($d) => $d->id !== $p->id);
if ($pool->isEmpty()) {
$pool = $nonDesperate;
}
if ($pool->isEmpty()) {
continue;
}
$candidate = $this->pickValidTarget($p, $pool, $assignments);
if ($candidate) {
$assignments[$p->id] = $candidate->id;
}
}
/*
|--------------------------------------------------------------------------
| 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,
]);
}
}
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;
}
}

View File

@ -8,6 +8,8 @@ state(['token'])->url();
state([
'participant' => null,
'userData' => null,
'receiverPrompt' => null,
'receiverUserData' => null,
'prompt' => '',
'isEditing' => false,
]);
@ -23,6 +25,10 @@ mount(function () {
if ($this->participant) {
$this->userData = $this->participant->getUserData();
$this->prompt = $this->participant->prompt ?? '';
if($this->participant->receiver) {
$this->receiverPrompt = $this->participant->receiver->prompt;
$this->receiverUserData = $this->participant->receiver->getUserData();
}
}
}
});
@ -82,6 +88,11 @@ $cancel = function () {
@if($participant->giving_id)
You are giving to <p>{{ $participant->giving_id }}</p>
@if($receiverUserData)
<img src ="{{ $receiverUserData['data']['profile_image_url'] ?? '' }}">
<h1> {{ $receiverUserData['data']['username'] ?? 'Unknown' }}</h1>
@endif
{{ "Their prompt is... {$receiverPrompt}" ?: 'No prompt set yet.' }}
@elseif($participant->desperate)
<p>You're in a bind!</p>
@else

View File

@ -1,8 +1,9 @@
<?php
use App\Models\Participant;
use App\Services\MatcherService;
use Illuminate\Database\Eloquent\Collection;
use function Livewire\Volt\{state, mount, rules};
use function Livewire\Volt\{state, mount};
state(['participants' => Collection::class]);
@ -10,14 +11,47 @@ mount(function () {
$this->participants = Participant::all();
});
$runMatch = function () {
$matcher = app(MatcherService::class);
$matcher->match();
session()->flash('match-message',"Done");
$this->participants = Participant::all();
};
?>
<div class="p-6 bg-gray-100 min-h-screen">
<div class="max-w-7xl mx-auto">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-extrabold text-gray-900">All Participants Data</h1>
<button
wire:click="runMatch"
wire:loading.attr="disabled"
wire:target="runMatch"
class="px-4 py-2 bg-indigo-600 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 transition duration-150 disabled:opacity-50"
>
<span wire:loading wire:target="runMatch">Processing Match...</span>
<span wire:loading.remove wire:target="runMatch">Run Matcher Service</span>
</button>
</div>
<p class="text-gray-600 mb-4">
Displaying all records from the `participants` table.
</p>
@if (session()->has('match-message'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-4 rounded" role="alert">
<p class="font-bold">Success!</p>
<p>{{ session('match-message') }}</p>
</div>
@endif
@if($participants->isEmpty())
<div class="p-4 text-center text-gray-500 bg-white shadow rounded-lg">
No record
No records
</div>
@else
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
@ -53,10 +87,10 @@ mount(function () {
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<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->desperate ? 'YES' : 'No' }}
</span>
<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->desperate ? 'YES' : 'No' }}
</span>
</td>
<td class="px-6 py-4 text-sm font-mono text-gray-500 truncate" title="{{ $participant->token }}">
@ -69,6 +103,3 @@ mount(function () {
</div>
@endif
</div>
</div>