- Schema: isConditional, conditionType, conditionText, conditionThreshold, conditionMet, conditionMetAt on Pledge - Pledge form: 'This is a match pledge' toggle after amount selection - Two modes: threshold (if target is reached) and match (match funding) - Goal amount passed through from event - Auto-trigger: when total raised hits threshold, conditional pledges unlock automatically - WhatsApp notification sent to donor when unlocked - Threshold check runs after every pledge creation AND every status change - Cron: skips conditional pledges until conditionMet=true (no premature reminders) - Dashboard Home: progress bar shows conditional segment (amber), stats grid adds Conditional column - Dashboard Money: conditional/unlocked badge on pledge rows - Dashboard Collect: hero shows conditional total in amber - Dashboard Reports: financial summary shows conditional breakdown - Donor 'My Pledges': conditional card with condition text + activation status - Confirmation step: specialized messaging for match pledges - CRM export: includes is_conditional, condition_type, condition_text, condition_met columns - Status guide: conditional status explained in human language
261 lines
15 KiB
PHP
261 lines
15 KiB
PHP
<x-filament-panels::page>
|
||
@php
|
||
$global = $this->getGlobalStats();
|
||
$campaigns = $this->getCampaignData();
|
||
$failed = $this->getFailedPayments();
|
||
$upcoming = $this->getUpcomingPayments();
|
||
$quality = $this->getDataQuality();
|
||
@endphp
|
||
|
||
{{-- ── Current Season Overview ─────────────────────────────── --}}
|
||
<x-filament::section>
|
||
<x-slot name="heading">
|
||
<div class="flex items-center gap-2">
|
||
<x-heroicon-o-sun class="w-5 h-5 text-warning-500" />
|
||
Ramadan {{ now()->year }} — Current Season
|
||
</div>
|
||
</x-slot>
|
||
|
||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||
<div class="text-center">
|
||
<div class="text-3xl font-bold text-primary-600">{{ number_format($global['current_subscribers']) }}</div>
|
||
<div class="text-sm text-gray-500 mt-1">Active</div>
|
||
<div class="text-xs text-gray-400">{{ number_format($global['expired_subscribers']) }} past seasons</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-3xl font-bold text-success-600">£{{ number_format($global['collected'], 0) }}</div>
|
||
<div class="text-sm text-gray-500 mt-1">Collected</div>
|
||
<div class="text-xs text-gray-400">{{ number_format($global['paid_payments']) }}/{{ number_format($global['due_payments']) }} due paid</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-3xl font-bold {{ $global['failed_count'] > 0 ? 'text-danger-600' : 'text-gray-400' }}">£{{ number_format($global['failed_amount'], 0) }}</div>
|
||
<div class="text-sm text-gray-500 mt-1">Failed</div>
|
||
<div class="text-xs text-danger-500">{{ number_format($global['failed_count']) }} payments</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-3xl font-bold text-gray-500">£{{ number_format($global['scheduled_amount'], 0) }}</div>
|
||
<div class="text-sm text-gray-500 mt-1">Upcoming</div>
|
||
<div class="text-xs text-gray-400">{{ number_format($global['scheduled_count']) }} not yet due</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-3xl font-bold {{ $global['collection_rate'] >= 80 ? 'text-success-600' : ($global['collection_rate'] >= 60 ? 'text-warning-600' : 'text-danger-600') }}">
|
||
{{ $global['collection_rate'] }}%
|
||
</div>
|
||
<div class="text-sm text-gray-500 mt-1">Collection Rate</div>
|
||
<div class="text-xs text-gray-400">of due payments</div>
|
||
</div>
|
||
</div>
|
||
</x-filament::section>
|
||
|
||
{{-- ── Campaign Cards ──────────────────────────────────────── --}}
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6 mb-6">
|
||
@foreach ($campaigns as $data)
|
||
@php
|
||
$c = $data['campaign'];
|
||
$hasCurrent = $data['current_subscribers'] > 0;
|
||
$duePct = $data['due_payments'] > 0
|
||
? round($data['paid_payments'] / $data['due_payments'] * 100)
|
||
: 0;
|
||
$overallPct = $data['total_payments'] > 0
|
||
? round($data['paid_payments'] / $data['total_payments'] * 100)
|
||
: 0;
|
||
@endphp
|
||
|
||
<x-filament::section>
|
||
<x-slot name="heading">
|
||
<div class="flex items-center justify-between">
|
||
<span>{{ $c->title }}</span>
|
||
@if ($hasCurrent)
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">● Active</span>
|
||
@else
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">○ No current season</span>
|
||
@endif
|
||
</div>
|
||
</x-slot>
|
||
|
||
@if ($hasCurrent)
|
||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||
<div>
|
||
<div class="text-xs text-gray-500 uppercase tracking-wide">Subscribers</div>
|
||
<div class="text-lg font-semibold">{{ $data['current_subscribers'] }}</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs text-gray-500 uppercase tracking-wide">Avg / Night</div>
|
||
<div class="text-lg font-semibold">£{{ number_format($data['avg_per_night'], 2) }}</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs text-gray-500 uppercase tracking-wide">Collected</div>
|
||
<div class="text-lg font-semibold text-success-600">£{{ number_format($data['collected'], 0) }}</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs text-gray-500 uppercase tracking-wide">
|
||
{{ $data['collection_rate'] }}% Rate
|
||
</div>
|
||
<div class="text-lg font-semibold {{ $data['collection_rate'] >= 80 ? 'text-success-600' : ($data['collection_rate'] >= 60 ? 'text-warning-600' : 'text-danger-600') }}">
|
||
{{ $data['paid_payments'] }}/{{ $data['due_payments'] }} due
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Progress bar: paid / total (including future) --}}
|
||
<div class="mb-3">
|
||
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
||
<span>{{ number_format($data['paid_payments']) }} paid, {{ number_format($data['failed_payments']) }} failed, {{ number_format($data['scheduled_payments']) }} upcoming</span>
|
||
</div>
|
||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700 flex overflow-hidden">
|
||
@if ($data['total_payments'] > 0)
|
||
<div class="h-2.5 bg-success-500" style="width: {{ $data['paid_payments'] / $data['total_payments'] * 100 }}%"></div>
|
||
<div class="h-2.5 bg-danger-400" style="width: {{ $data['failed_payments'] / $data['total_payments'] * 100 }}%"></div>
|
||
@endif
|
||
</div>
|
||
<div class="flex justify-between text-xs mt-1">
|
||
<span class="text-success-600">■ Paid</span>
|
||
@if ($data['failed_payments'] > 0)
|
||
<span class="text-danger-500">■ Failed (£{{ number_format($data['failed_amount'], 0) }})</span>
|
||
@endif
|
||
<span class="text-gray-400">■ Upcoming (£{{ number_format($data['scheduled_amount'], 0) }})</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-3 gap-2 text-center border-t pt-3 dark:border-gray-700">
|
||
<div>
|
||
<div class="text-xs text-gray-500">Nights</div>
|
||
<div class="font-semibold">{{ $data['total_nights'] }}</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs text-gray-500">Completed</div>
|
||
<div class="font-semibold text-success-600">{{ $data['fully_completed'] }}</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-xs text-gray-500">Failed</div>
|
||
<div class="font-semibold {{ $data['failed_payments'] > 0 ? 'text-danger-600' : 'text-gray-400' }}">{{ $data['failed_payments'] }}</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- All-time --}}
|
||
<div class="{{ $hasCurrent ? 'mt-3 pt-3 border-t dark:border-gray-700' : '' }}">
|
||
<div class="text-xs font-medium text-gray-400 uppercase tracking-wide mb-1">All Time</div>
|
||
<div class="flex justify-between text-sm text-gray-500">
|
||
<span>{{ $data['all_time_subscribers'] }} subscribers ({{ $data['expired_subscribers'] }} expired)</span>
|
||
<span class="font-semibold">£{{ number_format($data['all_time_collected'], 0) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2 mt-3 pt-3 border-t dark:border-gray-700">
|
||
<a href="{{ url('/admin/scheduled-giving-donations?activeTab=' . Str::slug($c->title)) }}"
|
||
class="text-xs text-primary-600 hover:underline">Subscribers →</a>
|
||
<a href="{{ url('/admin/scheduled-giving-campaigns/' . $c->id . '/edit') }}"
|
||
class="text-xs text-gray-500 hover:underline ml-auto">Config →</a>
|
||
</div>
|
||
</x-filament::section>
|
||
@endforeach
|
||
</div>
|
||
|
||
{{-- ── Upcoming Payments ───────────────────────────────────── --}}
|
||
@if (count($upcoming) > 0)
|
||
<x-filament::section class="mb-6">
|
||
<x-slot name="heading">
|
||
<div class="flex items-center gap-2">
|
||
<x-heroicon-o-clock class="w-5 h-5 text-primary-500" />
|
||
Upcoming Payments (Next 48h)
|
||
</div>
|
||
</x-slot>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
@foreach ($upcoming as $u)
|
||
<div class="flex items-center justify-between p-3 bg-primary-50 dark:bg-primary-950 rounded-lg">
|
||
<div>
|
||
<div class="font-semibold">{{ $u->campaign }}</div>
|
||
<div class="text-sm text-gray-500">{{ $u->payment_count }} payments</div>
|
||
</div>
|
||
<div class="text-right">
|
||
<div class="font-bold text-primary-600">£{{ number_format($u->total_amount, 2) }}</div>
|
||
<div class="text-xs text-gray-500">{{ \Carbon\Carbon::parse($u->earliest)->diffForHumans() }}</div>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</x-filament::section>
|
||
@endif
|
||
|
||
{{-- ── Failed Payments ─────────────────────────────────────── --}}
|
||
@if (count($failed) > 0)
|
||
<x-filament::section class="mb-6">
|
||
<x-slot name="heading">
|
||
<div class="flex items-center gap-2">
|
||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-danger-500" />
|
||
Failed Payments — This Season ({{ count($failed) }})
|
||
</div>
|
||
</x-slot>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-sm">
|
||
<thead>
|
||
<tr class="text-left text-xs text-gray-500 uppercase border-b dark:border-gray-700">
|
||
<th class="pb-2">Donor</th>
|
||
<th class="pb-2">Campaign</th>
|
||
<th class="pb-2">Amount</th>
|
||
<th class="pb-2">Expected</th>
|
||
<th class="pb-2">Attempts</th>
|
||
<th class="pb-2"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($failed as $f)
|
||
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900">
|
||
<td class="py-2">
|
||
<div class="font-medium">{{ $f->donor_name }}</div>
|
||
<div class="text-xs text-gray-400">{{ $f->donor_email }}</div>
|
||
</td>
|
||
<td class="py-2">{{ $f->campaign }}</td>
|
||
<td class="py-2 font-semibold">£{{ number_format($f->amount / 100, 2) }}</td>
|
||
<td class="py-2 text-gray-500">{{ \Carbon\Carbon::parse($f->expected_at)->format('d M') }}</td>
|
||
<td class="py-2">
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300">
|
||
{{ $f->attempts }}×
|
||
</span>
|
||
</td>
|
||
<td class="py-2">
|
||
<a href="{{ url('/admin/scheduled-giving-donations/' . $f->donation_id . '/edit') }}"
|
||
class="text-xs text-primary-600 hover:underline">View →</a>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</x-filament::section>
|
||
@endif
|
||
|
||
{{-- ── Data Quality ────────────────────────────────────────── --}}
|
||
<x-filament::section collapsible collapsed>
|
||
<x-slot name="heading">
|
||
<div class="flex items-center gap-2">
|
||
<x-heroicon-o-shield-exclamation class="w-5 h-5 text-gray-400" />
|
||
Data Quality
|
||
</div>
|
||
</x-slot>
|
||
<p class="text-sm text-gray-500 mb-3">
|
||
{{ number_format($quality['total_records']) }} total records in database.
|
||
Only {{ number_format($global['total_subscribers']) }} are real subscribers with payments.
|
||
</p>
|
||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-center text-sm">
|
||
<div class="p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div class="font-bold">{{ number_format($quality['soft_deleted']) }}</div>
|
||
<div class="text-xs text-gray-500">Soft-deleted</div>
|
||
</div>
|
||
<div class="p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div class="font-bold">{{ number_format($quality['no_customer']) }}</div>
|
||
<div class="text-xs text-gray-500">No customer</div>
|
||
</div>
|
||
<div class="p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div class="font-bold">{{ number_format($quality['no_payments']) }}</div>
|
||
<div class="text-xs text-gray-500">No payments</div>
|
||
</div>
|
||
<div class="p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div class="font-bold">{{ number_format($quality['zero_amount']) }}</div>
|
||
<div class="text-xs text-gray-500">Zero amount</div>
|
||
</div>
|
||
</div>
|
||
</x-filament::section>
|
||
</x-filament-panels::page>
|