Files
calvana/temp_files/fix2/scheduled-giving-dashboard.blade.php
Omair Saleh c11bf4bea7 Fundamental Collect redesign: unified creation, payment clarity, widget embed
THE CORE PROBLEM:
Users didn't understand the appeal→link hierarchy.
Payment method was hidden inside appeal creation.
The widget was a separate, undiscoverable concept.
External platforms (JustGiving, LaunchGood) felt disconnected.

THE FIX:

1. ONE CREATION FLOW for everything:
   Step 1: 'What are you raising for?' → creates the appeal
   Step 2: 'How will donors pay?' → 3 big clear cards:
     - Bank transfer (most popular, free)
     - External platform (JustGiving, LaunchGood, etc.)
     - Card payment (Stripe)
   Step 3: 'Name your link' → shows summary, creates both

2. PAYMENT METHOD VISIBLE ON EVERY LINK:
   Each link card shows a badge: 'Bank' or 'JustGiving' etc.
   External links show 'After pledging, donors are sent to...'
   No confusion about how money flows.

3. WIDGET IS A SHARING TAB, NOT A SEPARATE CONCEPT:
   Every link card expands to show 3 tabs:
     - Link (copy URL, WhatsApp, email, share)
     - QR Code (download PNG for printing)
     - Website Widget (iframe embed code with copy button)
   The widget is just another way to share the same link.

4. FLAT LINK LIST (not appeal→link hierarchy):
   All links shown in one flat list
   Appeal name shown as subtitle when multiple appeals exist
   'New link' adds to existing appeal
   'New appeal' uses the full 3-step wizard

5. EDUCATIONAL RIGHT COLUMN:
   'How it works' 5-step guide
   'Which payment method should I choose?' comparison
   'Can I mix payment methods?' FAQ
   'What's an appeal?' explanation (demystifies the concept)
   Leaderboard when 3+ links have pledges
2026-03-05 04:00:14 +08:00

245 lines
14 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-4 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 This Season</div>
<div class="text-xs text-gray-400">{{ number_format($global['expired_subscribers']) }} from past seasons</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-success-600">£{{ number_format($global['current_collected'], 0) }}</div>
<div class="text-sm text-gray-500 mt-1">Collected This Season</div>
<div class="text-xs text-gray-400">£{{ number_format($global['all_time_collected'], 0) }} all-time</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-warning-600">£{{ number_format($global['current_pending'], 0) }}</div>
<div class="text-sm text-gray-500 mt-1">Pending</div>
@if ($global['current_failed'] > 0)
<div class="text-xs text-danger-500">{{ $global['current_failed'] }} failed</div>
@endif
</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>
</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;
$progressPct = $data['current_payments'] > 0
? round($data['current_paid'] / $data['current_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)
{{-- Current Season --}}
<div class="text-xs font-medium text-primary-600 uppercase tracking-wide mb-2">This Season</div>
<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['current_collected'], 0) }}</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Pending</div>
<div class="text-lg font-semibold text-warning-600">£{{ number_format($data['current_pending_amount'], 0) }}</div>
</div>
</div>
{{-- Payment progress bar --}}
<div class="mb-3">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>{{ number_format($data['current_paid']) }} / {{ number_format($data['current_payments']) }} payments</span>
<span>{{ $progressPct }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div class="h-2.5 rounded-full {{ $progressPct >= 80 ? 'bg-success-500' : ($progressPct >= 50 ? 'bg-warning-500' : 'bg-primary-500') }}"
style="width: {{ $progressPct }}%"></div>
</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['current_failed'] > 0 ? 'text-danger-600' : 'text-gray-400' }}">{{ $data['current_failed'] }}</div>
</div>
</div>
@endif
{{-- All-time summary --}}
<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.
The rest are incomplete sign-ups, test data, or soft-deleted.
</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>