Commit 9781062a authored by Mahmoud Aglan's avatar Mahmoud Aglan

Allow siblings to share membership_id — drop unique constraint, add confirmation

Siblings in the same family share a club membership number. The unique
constraint on (branch_id, membership_id) is dropped. When a receptionist
or admin enters a membership_id that already exists, a confirmation prompt
shows the existing participant's full name so staff can verify it's a
sibling before proceeding. Both the wizard and participant form are updated.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 6f2307c5
......@@ -37,6 +37,8 @@ class ParticipantForm extends Component
public ?string $skill_level = null;
public ?string $membership_type = null;
public string $membership_id = '';
public bool $membershipIdSiblingConfirmed = false;
public array $membershipIdSiblings = [];
public bool $medical_clearance = false;
public ?string $medical_clearance_expires = null;
public ?int $jersey_number = null;
......@@ -174,20 +176,43 @@ public function messages(): array
];
}
public function confirmMembershipSibling(): void
{
$this->membershipIdSiblingConfirmed = true;
$this->membershipIdSiblings = [];
}
public function cancelMembershipSibling(): void
{
$this->membershipIdSiblings = [];
$this->membership_id = '';
}
public function updatedMembershipId(): void
{
$this->membershipIdSiblingConfirmed = false;
$this->membershipIdSiblings = [];
}
public function save(ParticipantService $service): void
{
$this->validate();
// Validate membership_id uniqueness per branch
if ($this->membership_type === 'member' && $this->membership_id) {
// Sibling check — warn if membership_id already in use, require confirmation
if ($this->membership_type === 'member' && $this->membership_id && !$this->membershipIdSiblingConfirmed) {
$branchId = $this->branch_id ?? auth()->user()->branch_id;
$exists = Participant::where('branch_id', $branchId)
$existingSiblings = Participant::where('branch_id', $branchId)
->where('membership_id', $this->membership_id)
->when($this->editing, fn ($q) => $q->where('id', '!=', $this->participant->id))
->exists();
->whereNull('deleted_at')
->with('person')
->get();
if ($exists) {
$this->addError('membership_id', 'رقم العضوية مستخدم بالفعل لمشترك آخر');
if ($existingSiblings->isNotEmpty()) {
$this->membershipIdSiblings = $existingSiblings->map(fn ($p) => [
'id' => $p->id,
'name' => $p->person?->name_ar ?? '-',
])->toArray();
return;
}
}
......
......@@ -58,6 +58,8 @@ class NewRegistrationWizard extends Component
public string $participant_medical_notes = '';
public string $membership_type = 'non_member';
public string $membership_id = '';
public bool $membershipIdSiblingConfirmed = false;
public array $membershipIdSiblings = [];
public string $participant_governorate = '';
public bool $participant_is_foreign = false;
public bool $participant_nid_decoded = false;
......@@ -241,6 +243,25 @@ private function autoFillGuardianName(): void
}
}
public function confirmMembershipSibling(): void
{
$this->membershipIdSiblingConfirmed = true;
$this->membershipIdSiblings = [];
$this->nextStep();
}
public function cancelMembershipSibling(): void
{
$this->membershipIdSiblings = [];
$this->membership_id = '';
}
public function updatedMembershipId(): void
{
$this->membershipIdSiblingConfirmed = false;
$this->membershipIdSiblings = [];
}
// --- Step navigation ---
public function nextStep(): void
......@@ -251,13 +272,19 @@ public function nextStep(): void
$this->validate($rules, $this->messages());
}
// Membership ID uniqueness check on step 1
if ($this->currentStep === 1 && $this->membership_type === 'member' && $this->membership_id) {
$exists = Participant::where('branch_id', $this->branchId)
// Membership ID sibling check on step 1
if ($this->currentStep === 1 && $this->membership_type === 'member' && $this->membership_id && !$this->membershipIdSiblingConfirmed) {
$existingSiblings = Participant::where('branch_id', $this->branchId)
->where('membership_id', $this->membership_id)
->exists();
if ($exists) {
$this->addError('membership_id', 'رقم العضوية مستخدم بالفعل لمشترك آخر في هذا الفرع');
->whereNull('deleted_at')
->with('person')
->get();
if ($existingSiblings->isNotEmpty()) {
$this->membershipIdSiblings = $existingSiblings->map(fn ($p) => [
'id' => $p->id,
'name' => $p->person?->name_ar ?? '-',
])->toArray();
return;
}
}
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::statement("DROP INDEX IF EXISTS participants_branch_membership_id_unique");
}
public function down(): void
{
DB::statement("CREATE UNIQUE INDEX participants_branch_membership_id_unique ON participants (branch_id, membership_id) WHERE membership_id IS NOT NULL AND deleted_at IS NULL");
}
};
......@@ -203,11 +203,46 @@ class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2
@if($membership_type === 'member')
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('رقم عضوية النادي') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="membership_id" dir="ltr"
<input type="text" wire:model.live.debounce.500ms="membership_id" dir="ltr"
placeholder="{{ __('أدخل رقم العضوية') }}"
class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('membership_id') border-red-500 @enderror">
@error('membership_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Sibling confirmation --}}
@if(!empty($membershipIdSiblings))
<div class="md:col-span-2">
<div class="bg-amber-50 border border-amber-300 rounded-xl p-4">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-amber-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<div class="flex-1">
<p class="text-sm font-semibold text-amber-800 mb-2">{{ __('رقم العضوية مسجل بالفعل للمشتركين التاليين:') }}</p>
@foreach($membershipIdSiblings as $sibling)
<div class="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-amber-200 mb-1">
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<span class="text-sm font-medium text-gray-800">{{ $sibling['name'] }}</span>
</div>
@endforeach
<p class="text-sm text-amber-700 mt-2 mb-3">{{ __('هل هذا المشترك أخو/أخت المشترك أعلاه؟') }}</p>
<div class="flex gap-3">
<button type="button" wire:click="confirmMembershipSibling"
class="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition">
{{ __('نعم، أكمل') }}
</button>
<button type="button" wire:click="cancelMembershipSibling"
class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-300 transition">
{{ __('لا، غيّر الرقم') }}
</button>
</div>
</div>
</div>
</div>
</div>
@endif
@endif
</div>
</div>
......
......@@ -269,11 +269,50 @@ class="w-full px-4 py-3 min-h-[52px] border border-gray-300 rounded-lg focus:rin
@if($membership_type === 'member')
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('رقم عضوية النادي') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="membership_id" dir="ltr"
<input type="text" wire:model.live.debounce.500ms="membership_id" dir="ltr"
class="w-full px-4 py-3 min-h-[52px] border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-lg"
placeholder="{{ __('أدخل رقم العضوية') }}">
@error('membership_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Sibling confirmation prompt --}}
@if(!empty($membershipIdSiblings))
<div class="sm:col-span-2">
<div class="bg-amber-50 border border-amber-300 rounded-xl p-4">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-amber-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<div class="flex-1">
<p class="text-sm font-semibold text-amber-800 mb-2">{{ __('رقم العضوية مسجل بالفعل للمشتركين التاليين:') }}</p>
<div class="space-y-2 mb-3">
@foreach($membershipIdSiblings as $sibling)
<div class="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-amber-200">
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<span class="text-sm font-medium text-gray-800">{{ $sibling['name'] }}</span>
</div>
@endforeach
</div>
<p class="text-sm text-amber-700 mb-3">
{{ __('هل') }} <span class="font-bold">{{ $participant_name_ar }}</span> {{ __('أخو/أخت المشترك أعلاه ويشتركوا في نفس رقم العضوية؟') }}
</p>
<div class="flex gap-3">
<button type="button" wire:click="confirmMembershipSibling"
class="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition">
{{ __('نعم، إخوة — أكمل التسجيل') }}
</button>
<button type="button" wire:click="cancelMembershipSibling"
class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-300 transition">
{{ __('لا — غيّر رقم العضوية') }}
</button>
</div>
</div>
</div>
</div>
</div>
@endif
@endif
{{-- Medical notes --}}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment