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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ return new class extends Migration
|
|||
|
||||
$table->string('token');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ $runMatch = function () {
|
|||
|
||||
<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' }}
|
||||
{{ $participant->is_desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
|
||||
{{ $participant->is_desperate ? 'YES' : 'No' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
|
|
|||
|
|
@ -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")');
|
||||
|
|
|
|||
Loading…
Reference in New Issue