can match
This commit is contained in:
parent
8f5aab2c57
commit
a930d0f972
|
|
@ -23,12 +23,12 @@ class Participant extends Model
|
||||||
'desperate' => 'boolean',
|
'desperate' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function giver()
|
public function receiver()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Participant::class, 'giving_id');
|
return $this->belongsTo(Participant::class, 'giving_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function receiver()
|
public function giver()
|
||||||
{
|
{
|
||||||
return $this->hasOne(Participant::class, 'giving_id', 'id');
|
return $this->hasOne(Participant::class, 'giving_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ state(['token'])->url();
|
||||||
state([
|
state([
|
||||||
'participant' => null,
|
'participant' => null,
|
||||||
'userData' => null,
|
'userData' => null,
|
||||||
|
'receiverPrompt' => null,
|
||||||
|
'receiverUserData' => null,
|
||||||
'prompt' => '',
|
'prompt' => '',
|
||||||
'isEditing' => false,
|
'isEditing' => false,
|
||||||
]);
|
]);
|
||||||
|
|
@ -23,6 +25,10 @@ mount(function () {
|
||||||
if ($this->participant) {
|
if ($this->participant) {
|
||||||
$this->userData = $this->participant->getUserData();
|
$this->userData = $this->participant->getUserData();
|
||||||
$this->prompt = $this->participant->prompt ?? '';
|
$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)
|
@if($participant->giving_id)
|
||||||
You are giving to <p>{{ $participant->giving_id }}</p>
|
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)
|
@elseif($participant->desperate)
|
||||||
<p>You're in a bind!</p>
|
<p>You're in a bind!</p>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\Participant;
|
use App\Models\Participant;
|
||||||
|
use App\Services\MatcherService;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use function Livewire\Volt\{state, mount, rules};
|
use function Livewire\Volt\{state, mount};
|
||||||
|
|
||||||
state(['participants' => Collection::class]);
|
state(['participants' => Collection::class]);
|
||||||
|
|
||||||
|
|
@ -10,14 +11,47 @@ mount(function () {
|
||||||
$this->participants = Participant::all();
|
$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="p-6 bg-gray-100 min-h-screen">
|
||||||
<div class="max-w-7xl mx-auto">
|
<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())
|
@if($participants->isEmpty())
|
||||||
<div class="p-4 text-center text-gray-500 bg-white shadow rounded-lg">
|
<div class="p-4 text-center text-gray-500 bg-white shadow rounded-lg">
|
||||||
No record
|
No records
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||||
|
|
@ -53,10 +87,10 @@ mount(function () {
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<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->desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
|
||||||
{{ $participant->desperate ? 'YES' : 'No' }}
|
{{ $participant->desperate ? 'YES' : 'No' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4 text-sm font-mono text-gray-500 truncate" title="{{ $participant->token }}">
|
<td class="px-6 py-4 text-sm font-mono text-gray-500 truncate" title="{{ $participant->token }}">
|
||||||
|
|
@ -69,6 +103,3 @@ mount(function () {
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue