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
This commit is contained in:
2026-03-05 04:00:14 +08:00
parent dc8e593849
commit c11bf4bea7
13 changed files with 2203 additions and 567 deletions

View File

@@ -0,0 +1,244 @@
<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>