Commit 9d46be15 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add participant delete with per-invoice refund option

Super admin/academy owner can soft-delete participants from the list.
A modal shows all paid invoices and allows refunding each one individually
before confirming deletion. Cascading: cancels active enrollments, removes
future attendance records, cancels unpaid invoices, sets status to withdrawn.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 9b7cfaa9
<?php
namespace App\Livewire\Participants;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Services\PaymentService;
use App\Domain\Participant\Models\Participant;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\On;
use Livewire\Component;
class DeleteParticipant extends Component
{
public bool $showModal = false;
public ?int $participantId = null;
public ?string $participantName = null;
public array $paidInvoices = [];
public array $refundedInvoiceIds = [];
public ?int $confirmingRefundInvoiceId = null;
public bool $deleting = false;
#[On('open-delete-participant')]
public function open(int $id): void
{
$this->authorize('participants.delete');
$participant = Participant::with('person')->findOrFail($id);
$this->participantId = $id;
$this->participantName = $participant->person?->name_ar ?? $participant->person?->name ?? '-';
$this->refundedInvoiceIds = [];
$this->confirmingRefundInvoiceId = null;
$this->deleting = false;
$this->loadInvoices();
$this->showModal = true;
}
public function close(): void
{
$this->showModal = false;
$this->reset(['participantId', 'participantName', 'paidInvoices', 'refundedInvoiceIds', 'confirmingRefundInvoiceId', 'deleting']);
}
private function loadInvoices(): void
{
$invoices = Invoice::where('billable_type', Participant::class)
->where('billable_id', $this->participantId)
->where('paid_amount', '>', 0)
->with(['payments', 'items'])
->orderByDesc('created_at')
->get();
$this->paidInvoices = $invoices->map(fn ($inv) => [
'id' => $inv->id,
'number' => $inv->number,
'total_amount' => $inv->total_amount,
'paid_amount' => $inv->paid_amount,
'status' => $inv->status->value ?? $inv->status,
'issue_date' => $inv->issue_date?->format('Y-m-d'),
'description' => $inv->items->first()?->description ?? '-',
])->toArray();
}
public function askRefund(int $invoiceId): void
{
$this->confirmingRefundInvoiceId = $invoiceId;
}
public function cancelRefund(): void
{
$this->confirmingRefundInvoiceId = null;
}
public function confirmRefund(int $invoiceId): void
{
$this->authorize('participants.delete');
$invoice = Invoice::with('payments')->findOrFail($invoiceId);
$paymentService = app(PaymentService::class);
DB::transaction(function () use ($invoice, $paymentService) {
$confirmedPayments = $invoice->payments()
->where('direction', 'inbound')
->where('status', 'confirmed')
->get();
foreach ($confirmedPayments as $payment) {
$refundableAmount = $payment->amount - ($payment->refunded_amount ?? 0);
if ($refundableAmount > 0) {
$paymentService->refund($payment, $refundableAmount, auth()->user());
}
}
});
$this->refundedInvoiceIds[] = $invoiceId;
$this->confirmingRefundInvoiceId = null;
$this->loadInvoices();
}
public function deleteParticipant(): void
{
$this->authorize('participants.delete');
$this->deleting = true;
$participant = Participant::findOrFail($this->participantId);
DB::transaction(function () use ($participant) {
$participant->enrollments()
->whereIn('status', ['active', 'pending', 'waitlisted'])
->update(['status' => 'cancelled', 'cancelled_at' => now()]);
AttendanceRecord::where('subject_type', Participant::class)
->where('subject_id', $participant->id)
->where('status', 'expected')
->whereHas('session', fn ($q) => $q->where('session_date', '>=', now()->toDateString()))
->delete();
Invoice::where('billable_type', Participant::class)
->where('billable_id', $participant->id)
->where('paid_amount', 0)
->whereIn('status', ['draft', 'sent'])
->update(['status' => 'cancelled', 'cancelled_at' => now()]);
$participant->update(['status' => 'withdrawn']);
$participant->delete();
});
$this->showModal = false;
session()->flash('success', __('تم حذف المشترك بنجاح'));
$this->dispatch('participant-deleted');
}
public function render()
{
return view('livewire.participants.delete-participant');
}
}
......@@ -7,6 +7,7 @@
use App\Domain\Training\Models\Activity;
use App\Livewire\Concerns\AppliesRoleScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
......@@ -49,6 +50,12 @@ public function updatedSkillLevel(): void
$this->resetPage();
}
#[On('participant-deleted')]
public function refreshList(): void
{
// Livewire re-renders automatically
}
public function render()
{
$branchId = $this->getActiveBranchId();
......
<div>
@if($showModal)
<div class="fixed inset-0 z-50 overflow-y-auto" aria-modal="true">
<div class="flex items-center justify-center min-h-screen px-4 py-6">
{{-- Backdrop --}}
<div class="fixed inset-0 bg-black/50 transition-opacity" wire:click="close"></div>
{{-- Modal Content --}}
<div class="relative bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
{{-- Header --}}
<div class="px-6 py-4 border-b border-gray-200 bg-red-50">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</div>
<div>
<h3 class="text-lg font-bold text-gray-900">{{ __('حذف مشترك') }}</h3>
<p class="text-sm text-gray-600">{{ $participantName }}</p>
</div>
</div>
<button wire:click="close" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
{{-- Body --}}
<div class="px-6 py-4 overflow-y-auto flex-1">
{{-- Warning --}}
<div class="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p class="text-sm text-amber-800 font-medium">{{ __('تحذير: سيتم إلغاء جميع الاشتراكات النشطة وحذف سجلات الحضور المستقبلية.') }}</p>
</div>
{{-- Invoices Section --}}
@if(count($paidInvoices) > 0)
<div class="mb-4">
<h4 class="text-sm font-bold text-gray-700 mb-3">{{ __('الفواتير المدفوعة') }} ({{ count($paidInvoices) }})</h4>
<p class="text-xs text-gray-500 mb-3">{{ __('يمكنك استرداد المبالغ المدفوعة لكل فاتورة على حدة قبل الحذف، أو المتابعة بدون استرداد.') }}</p>
<div class="space-y-2">
@foreach($paidInvoices as $invoice)
<div class="border border-gray-200 rounded-lg p-3 {{ in_array($invoice['id'], $refundedInvoiceIds) ? 'bg-green-50 border-green-200' : '' }}">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-mono text-gray-600" dir="ltr">{{ $invoice['number'] }}</span>
@if(in_array($invoice['id'], $refundedInvoiceIds))
<span class="px-2 py-0.5 text-[10px] bg-green-100 text-green-700 rounded-full">{{ __('تم الاسترداد') }}</span>
@endif
</div>
<p class="text-xs text-gray-500 mt-0.5 truncate">{{ $invoice['description'] }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ $invoice['issue_date'] }}</p>
</div>
<div class="text-start flex-shrink-0">
<p class="text-sm font-bold text-gray-800">{{ number_format($invoice['paid_amount'] / 100, 2) }} {{ __('ج.م') }}</p>
@if(!in_array($invoice['id'], $refundedInvoiceIds))
@if($confirmingRefundInvoiceId === $invoice['id'])
<div class="flex items-center gap-1 mt-1">
<button wire:click="confirmRefund({{ $invoice['id'] }})"
wire:loading.attr="disabled"
class="px-2 py-1 text-[10px] bg-red-600 text-white rounded hover:bg-red-700 font-medium">
{{ __('تأكيد') }}
</button>
<button wire:click="cancelRefund"
class="px-2 py-1 text-[10px] bg-gray-200 text-gray-700 rounded hover:bg-gray-300">
{{ __('إلغاء') }}
</button>
</div>
@else
<button wire:click="askRefund({{ $invoice['id'] }})"
class="mt-1 px-2 py-1 text-[10px] bg-amber-100 text-amber-700 rounded hover:bg-amber-200 font-medium">
{{ __('استرداد') }}
</button>
@endif
@endif
</div>
</div>
</div>
@endforeach
</div>
</div>
@else
<div class="mb-4 p-4 bg-gray-50 rounded-lg text-center">
<p class="text-sm text-gray-500">{{ __('لا توجد فواتير مدفوعة لهذا المشترك') }}</p>
</div>
@endif
</div>
{{-- Footer --}}
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between gap-3">
<button wire:click="close"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
{{ __('إلغاء') }}
</button>
<button wire:click="deleteParticipant"
wire:loading.attr="disabled"
wire:target="deleteParticipant"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 disabled:opacity-50">
<span wire:loading.remove wire:target="deleteParticipant">{{ __('حذف المشترك نهائياً') }}</span>
<span wire:loading wire:target="deleteParticipant">{{ __('جارٍ الحذف...') }}</span>
</button>
</div>
</div>
</div>
</div>
@endif
</div>
<div>
@can('participants.delete')
<livewire:participants.delete-participant />
@endcan
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('المشتركين') }}</h1>
......@@ -121,6 +125,9 @@ class="w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 f
@can('participants.update')
<a href="{{ route('participants.edit', $participant) }}" wire:navigate class="text-green-600 hover:text-green-800 text-sm font-medium">{{ __('تعديل') }}</a>
@endcan
@can('participants.delete')
<button wire:click="$dispatch('open-delete-participant', { id: {{ $participant->id }} })" class="text-red-600 hover:text-red-800 text-sm font-medium" onclick="event.stopPropagation()">{{ __('حذف') }}</button>
@endcan
</div>
</td>
</tr>
......@@ -200,6 +207,12 @@ class="flex-1 text-center py-2 text-sm font-medium text-green-600 hover:text-gre
{{ __('تعديل') }}
</a>
@endcan
@can('participants.delete')
<button wire:click="$dispatch('open-delete-participant', { id: {{ $participant->id }} })"
class="flex-1 text-center py-2 text-sm font-medium text-red-600 hover:text-red-800 bg-red-50 rounded-lg transition-colors">
{{ __('حذف') }}
</button>
@endcan
</div>
</div>
@empty
......
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