Commit 9921dd22 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add overdue renewals alert banner to admin dashboard

Big red alert at the top of the dashboard showing participants who haven't
renewed their subscription. Each row links directly to the payment wizard
with participant pre-selected, auto-generating the renewal invoice.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e6a97e1c
<?php
namespace App\Livewire\Dashboard;
use App\Domain\Financial\Enums\InvoiceStatus;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Enums\EnrollmentStatus;
use App\Domain\Training\Enums\RenewalPolicy;
use App\Domain\Training\Models\Enrollment;
use Livewire\Component;
class OverdueRenewalsAlert extends Component
{
public bool $expanded = false;
public function toggleExpanded(): void
{
$this->expanded = !$this->expanded;
}
public function render()
{
$today = now()->toDateString();
// Find active enrollments with overdue billing that DON'T already have an unpaid invoice
$overdueEnrollments = Enrollment::where('status', EnrollmentStatus::Active)
->whereNotNull('next_billing_date')
->where('next_billing_date', '<=', $today)
->whereHas('program', fn ($q) => $q->whereIn('renewal_policy', [
RenewalPolicy::AutoRenew->value,
RenewalPolicy::ManualRenew->value,
])->whereNotNull('billing_cycle'))
->with(['participant.person', 'program', 'group.branch'])
->orderBy('next_billing_date')
->get();
// Filter out those who already have an unpaid invoice for this program
$overdueEnrollments = $overdueEnrollments->filter(function ($enrollment) {
if (!$enrollment->participant) {
return false;
}
$hasUnpaidInvoice = Invoice::where('billable_type', Participant::class)
->where('billable_id', $enrollment->participant_id)
->whereIn('status', [InvoiceStatus::Sent, InvoiceStatus::PartiallyPaid, InvoiceStatus::Overdue])
->where('due_amount', '>', 0)
->where('notes', 'like', '%' . ($enrollment->program?->name_ar ?? '') . '%')
->exists();
return !$hasUnpaidInvoice;
});
$totalOverdue = $overdueEnrollments->count();
$displayList = $this->expanded ? $overdueEnrollments : $overdueEnrollments->take(10);
return view('livewire.dashboard.overdue-renewals-alert', [
'overdueEnrollments' => $displayList,
'totalOverdue' => $totalOverdue,
]);
}
}
......@@ -5,6 +5,11 @@
<p class="text-gray-500 text-sm mt-1">{{ now()->translatedFormat('l j F Y') }} &mdash; {{ __('مرحبا') }}، {{ auth()->user()->name_ar ?? auth()->user()->name }}</p>
</div>
<!-- Overdue Renewals Alert Banner (top priority) -->
@can('invoices.create')
<livewire:dashboard.overdue-renewals-alert />
@endcan
<!-- Row 1: Today's Overview (5 stat cards) — auto-refresh every 30s -->
<div wire:poll.30s class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
<!-- Sessions Today -->
......
<div>
@if($totalOverdue > 0)
<div class="bg-gradient-to-l from-red-50 to-red-100 rounded-xl shadow-sm border-2 border-red-300 p-5 mb-6">
{{-- Header --}}
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-red-200 rounded-xl flex items-center justify-center animate-pulse">
<svg class="w-7 h-7 text-red-700" 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>
<div>
<h2 class="text-lg font-bold text-red-900">{{ __('اشتراكات لم تُجدد') }}</h2>
<p class="text-sm text-red-700">
<span class="font-bold text-xl">{{ $totalOverdue }}</span>
{{ __('مشترك لم يجدد اشتراكه حتى الآن') }}
</p>
</div>
</div>
<a href="{{ route('receptionist.collect-payment') }}" wire:navigate
class="inline-flex items-center gap-2 px-5 py-3 bg-red-700 text-white rounded-xl hover:bg-red-800 text-sm font-bold transition-colors shadow-sm">
<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="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ __('تحصيل مدفوعات') }}
</a>
</div>
{{-- Participants list --}}
<div class="bg-white rounded-xl border border-red-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-red-50 border-b border-red-200">
<tr>
<th class="text-start px-4 py-2.5 font-semibold text-red-800">{{ __('المشترك') }}</th>
<th class="text-start px-4 py-2.5 font-semibold text-red-800">{{ __('البرنامج') }}</th>
<th class="text-start px-4 py-2.5 font-semibold text-red-800 hidden sm:table-cell">{{ __('الفرع') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('تأخير') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('إجراء') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-red-100">
@foreach($overdueEnrollments as $enrollment)
<tr class="hover:bg-red-50 transition-colors">
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center shrink-0">
<svg class="w-4 h-4 text-red-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>
</div>
<div class="min-w-0">
<p class="font-medium text-gray-800 truncate">{{ $enrollment->participant?->person?->name_ar ?? '-' }}</p>
@if($enrollment->participant?->person?->phone)
<p class="text-xs text-gray-500" dir="ltr">{{ $enrollment->participant->person->phone }}</p>
@endif
</div>
</div>
</td>
<td class="px-4 py-3 text-gray-700">{{ $enrollment->program?->name_ar ?? '-' }}</td>
<td class="px-4 py-3 text-gray-500 hidden sm:table-cell">{{ $enrollment->group?->branch?->name_ar ?? '-' }}</td>
<td class="px-4 py-3 text-center">
@php $daysDue = (int) now()->diffInDays($enrollment->next_billing_date); @endphp
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold
{{ $daysDue > 7 ? 'bg-red-200 text-red-800' : 'bg-amber-100 text-amber-800' }}">
{{ $daysDue }} {{ __('يوم') }}
</span>
</td>
<td class="px-4 py-3 text-center">
@if($enrollment->participant?->uuid)
<a href="{{ route('receptionist.collect-payment') }}?participant={{ $enrollment->participant->uuid }}" wire:navigate
class="inline-flex items-center gap-1 px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xs font-bold transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8V7m0 10v1"/>
</svg>
{{ __('جدّد') }}
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Show more / less --}}
@if($totalOverdue > 10)
<div class="mt-3 text-center">
<button wire:click="toggleExpanded" class="text-sm text-red-700 hover:text-red-900 font-medium">
@if($expanded)
{{ __('عرض أقل') }}
@else
{{ __('عرض الكل') }} ({{ $totalOverdue }})
@endif
</button>
</div>
@endif
</div>
@endif
</div>
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