Files
calvana/temp_files/fix2/scheduled-giving-dashboard.blade.php
Omair Saleh 50d449e2b7 feat: conditional & match funding pledges — deeply integrated across entire product
- 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
2026-03-05 04:19:23 +08:00

261 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>