updategroup

This commit is contained in:
sandigmacorp-boop 2026-02-20 16:10:13 +07:00
parent 62f96023e9
commit ef989121ba
5 changed files with 1583 additions and 6 deletions

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\Group;
use Illuminate\Http\Request;
class GroupsController extends Controller
{
public function index(Request $request, int $eventId)
{
$q = Group::where('event_id', $eventId)
->withCount('registrations')
->with(['registrations.player' => function($q){ $q->select('id','name','phone','email'); }])
->orderByDesc('id');
if ($request->filled('search')) {
$s = trim($request->get('search'));
$q->where('group_code', 'ilike', "%{$s}%");
}
$items = $q->paginate((int)($request->get('per_page', 25)));
// Normalize response
$items->getCollection()->transform(function($g){
return [
'id' => $g->id,
'group_code' => $g->group_code,
'size_target' => (int)$g->size_target,
'status' => $g->status,
'registrations_count' => (int)$g->registrations_count,
'members' => $g->registrations->map(function($r){
return [
'registration_id' => $r->id,
'type' => $r->type,
'status' => $r->status,
'name' => $r->player?->name,
'phone' => $r->player?->phone,
'email' => $r->player?->email,
];
})->values(),
];
});
return response()->json($items);
}
}

View File

@ -17,18 +17,51 @@ class PairingController extends Controller
{ {
$code = $request->validated()['registration_code']; $code = $request->validated()['registration_code'];
$registration = Registration::where('registration_code', $code)->with('player')->firstOrFail(); $registration = Registration::where('registration_code', $code)
->with('player', 'flightMember.flight.members.registration.player')
->firstOrFail();
// Mask phone for UI: +62***1234 $flightMember = $registration->flightMember;
$phone = $registration->player->phone; $flight = $flightMember?->flight;
$masked = $this->maskPhone($phone);
$members = [];
if ($flight) {
foreach ($flight->members as $m) {
$members[] = [
'seat' => (int) $m->seat_no,
'name' => $m->registration?->player?->name,
'band' => $m->registration?->handicap_band,
'hcp' => $m->registration?->handicap_value,
'is_you' => $m->registration_id === $registration->id,
];
}
}
return response()->json([ return response()->json([
'registration_id' => $registration->id, 'registration' => [
'masked_phone' => $masked, 'status' => $registration->status,
'player' => [
'name' => $registration->player->name,
'band' => $registration->handicap_band,
'handicap' => $registration->handicap_value,
],
'preferences' => [
'pairing_mode' => $registration->pairing_mode,
'pairing_mode_label' => strtolower($registration->pairing_mode) === 'LEVEL' ? 'level' : (strtolower($registration->pairing_mode) === 'RANDOM' ? 'random' : 'balanced'),
],
],
'flight' => $flight ? [
'code' => $flight->code,
'course' => $flight->course,
'start_hole' => (int) $flight->start_hole,
'start_tee' => $flight->start_tee,
'is_final' => $flight->status === 'FINAL',
] : null,
'members' => $members,
]); ]);
} }
public function requestOtp(PairingOtpRequest $request, OtpService $otpService) public function requestOtp(PairingOtpRequest $request, OtpService $otpService)
{ {
$registrationId = $request->validated()['registration_id']; $registrationId = $request->validated()['registration_id'];

View File

@ -102,6 +102,108 @@ class PairingEngine
} }
} }
// 1b) Group-of-2/3 placement (keep same course; prefer same flight)
if ($groupIds->isNotEmpty()) {
$groups23 = Group::whereIn('id', $groupIds)->whereIn('size_target', [2,3])->get();
foreach ($groups23 as $g) {
$need = (int) $g->size_target;
$groupRegs = Registration::where('group_id', $g->id)
->where('status', 'CONFIRMED')
->whereDoesntHave('flightMember')
->with('player')
->get();
// Only place when exact members exist (2 or 3)
if ($groupRegs->count() !== $need) continue;
// derive coursePref from leader if any
$coursePref = 'ANY';
$leader = $groupRegs->firstWhere('type', 'GROUP_LEADER') ?? $groupRegs->first();
if ($leader && in_array($leader->course_pref, ['A','B'])) $coursePref = $leader->course_pref;
// 1) Try same flight with enough seats
$flightId = $this->findFlightWithAtLeastSeats($flights, $flightSeats, $courseLoad, $coursePref, $need);
if (!$flightId && $coursePref !== 'ANY') {
$flightId = $this->findFlightWithAtLeastSeats($flights, $flightSeats, $courseLoad, 'ANY', $need);
}
if ($flightId) {
$seats = array_slice($flightSeats[$flightId], 0, $need);
for ($i=0; $i<$need; $i++) {
$r = $groupRegs[$i];
FlightMember::create([
'flight_id' => $flightId,
'registration_id' => $r->id,
'seat_no' => $seats[$i],
'lock_flag' => false,
]);
$assigned++;
$courseLoad[$flights[$flightId]->course] = ($courseLoad[$flights[$flightId]->course] ?? 0) + 1;
if ($this->isHighLike($r->handicap_band)) {
$h = (int)$flights[$flightId]->start_hole;
$holeHighLikeLoad[$h] = ($holeHighLikeLoad[$h] ?? 0) + 1;
}
}
// consume seats + status
$flightSeats[$flightId] = array_values(array_slice($flightSeats[$flightId], $need));
$this->refreshFlightStatus($flightId);
$notes[] = "group {$g->group_code} -> flight {$flights[$flightId]->code}";
continue;
}
// 2) If no single flight can fit, split but keep SAME COURSE
$course = $this->pickCourseForGroupSplit($flights, $flightSeats, $courseLoad, $coursePref, $need);
if (!$course) {
$notes[] = "no seats available to place group {$g->group_code}";
continue;
}
$placed = 0;
foreach ($flights as $f) {
if ($f->course !== $course) continue;
if (empty($flightSeats[$f->id] ?? [])) continue;
while (!empty($flightSeats[$f->id]) && $placed < $need) {
$seat = array_shift($flightSeats[$f->id]);
$r = $groupRegs[$placed];
FlightMember::create([
'flight_id' => $f->id,
'registration_id' => $r->id,
'seat_no' => $seat,
'lock_flag' => false,
]);
$assigned++;
$courseLoad[$course] = ($courseLoad[$course] ?? 0) + 1;
if ($this->isHighLike($r->handicap_band)) {
$h = (int)$f->start_hole;
$holeHighLikeLoad[$h] = ($holeHighLikeLoad[$h] ?? 0) + 1;
}
$this->refreshFlightStatus($f->id);
$placed++;
}
if ($placed >= $need) break;
}
if ($placed >= $need) {
$notes[] = "group {$g->group_code} -> split in course {$course}";
} else {
$notes[] = "group {$g->group_code} placement incomplete (placed {$placed}/{$need})";
}
}
}
// refresh remaining regs // refresh remaining regs
$regs = $getPendingRegs(); $regs = $getPendingRegs();
@ -481,4 +583,64 @@ class PairingEngine
foreach (['LOW','MID'] as $b) if (!empty($pool[$b])) return array_shift($pool[$b]); foreach (['LOW','MID'] as $b) if (!empty($pool[$b])) return array_shift($pool[$b]);
return null; return null;
} }
private function findFlightWithAtLeastSeats($flights, array $flightSeats, array $courseLoad, string $coursePref, int $need): ?int
{
$candidates = [];
foreach ($flights as $f) {
$avail = count($flightSeats[$f->id] ?? []);
if ($avail < $need) continue;
if (in_array($coursePref, ['A','B']) && $f->course !== $coursePref) continue;
// prefer less-filled flights; if ANY, keep course balanced
$count = $f->members->count();
$score = (4 - $count) * 40;
if (config('pxg.pairing.course_balance', true) && $coursePref === 'ANY') {
$score += (200 - ($courseLoad[$f->course] ?? 0));
}
$candidates[] = [$f->id, $score];
}
if (empty($candidates)) return null;
usort($candidates, fn($x,$y) => $y[1] <=> $x[1]);
return $candidates[0][0];
}
private function pickCourseForGroupSplit($flights, array $flightSeats, array $courseLoad, string $coursePref, int $need): ?string
{
// if preferred course has enough total seats, choose it
if (in_array($coursePref, ['A','B'])) {
$total = 0;
foreach ($flights as $f) {
if ($f->course !== $coursePref) continue;
$total += count($flightSeats[$f->id] ?? []);
}
if ($total >= $need) return $coursePref;
}
// else choose course with more available seats (and/or lower load)
$seatA = 0; $seatB = 0;
foreach ($flights as $f) {
$cnt = count($flightSeats[$f->id] ?? []);
if ($f->course === 'A') $seatA += $cnt;
if ($f->course === 'B') $seatB += $cnt;
}
if ($seatA === 0 && $seatB === 0) return null;
if ($seatA >= $need && $seatB < $need) return 'A';
if ($seatB >= $need && $seatA < $need) return 'B';
// both can fit or neither fully fits -> keep balance (lower load), else more seats
if (config('pxg.pairing.course_balance', true)) {
$loadA = $courseLoad['A'] ?? 0;
$loadB = $courseLoad['B'] ?? 0;
return $loadA <= $loadB ? 'A' : 'B';
}
return $seatA >= $seatB ? 'A' : 'B';
}
} }

1332
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ use App\Http\Controllers\Api\V1\Admin\PaymentsController as AdminPaymentsControl
use App\Http\Controllers\Api\V1\Admin\PairingAdminController; use App\Http\Controllers\Api\V1\Admin\PairingAdminController;
use App\Http\Controllers\Api\V1\Admin\EventAdminController; use App\Http\Controllers\Api\V1\Admin\EventAdminController;
use App\Http\Controllers\Api\V1\Admin\ImportController; use App\Http\Controllers\Api\V1\Admin\ImportController;
use App\Http\Controllers\Api\V1\Admin\GroupsController as AdminGroupsController;
@ -43,6 +44,7 @@ Route::middleware('auth:sanctum')->group(function () {
Route::get('/admin/events/{event}/dashboard', [DashboardController::class, 'show']); Route::get('/admin/events/{event}/dashboard', [DashboardController::class, 'show']);
Route::get('/admin/events/{event}/registrations', [AdminRegistrationsController::class, 'index']); Route::get('/admin/events/{event}/registrations', [AdminRegistrationsController::class, 'index']);
Route::get('/admin/events/{event}/groups', [AdminGroupsController::class, 'index']);
Route::patch('/admin/registrations/{registration}', [AdminRegistrationsController::class, 'update']); Route::patch('/admin/registrations/{registration}', [AdminRegistrationsController::class, 'update']);
Route::get('/admin/events/{event}/flights', [AdminFlightsController::class, 'index']); Route::get('/admin/events/{event}/flights', [AdminFlightsController::class, 'index']);