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:
244
temp_files/fix2/scheduled-giving-dashboard.blade.php
Normal file
244
temp_files/fix2/scheduled-giving-dashboard.blade.php
Normal 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>
|
||||
Reference in New Issue
Block a user