Files
calvana/temp_files/DonationStatsWidget.php
Omair Saleh a9b3b70dfc Telepathic Collect: link-first, flattened hierarchy, embedded leaderboard
Core insight: The primary object is the LINK, not the appeal.
Aaisha doesn't think 'manage appeals' — she thinks 'share my link'.

## Collect page (/dashboard/collect) — complete rewrite
- Flattened hierarchy: single-appeal orgs see links directly (no card to click)
- Multi-appeal orgs: quiet appeal switcher at top, links below
- Inline link creation: just type a name + press Enter (no dialog)
- Quick preset buttons: 'Table 1', 'WhatsApp Group', 'Instagram', etc.
- Share buttons are THE primary CTA on every link card (Copy, WhatsApp, Email, Share)
- Each link shows: clicks, pledges, amount raised, conversion rate
- Embedded mini-leaderboard when 3+ links have pledges
- Contextual tips when pledges < 5 ('give each volunteer their own link')
- New appeal creation is inline, auto-creates 'Main link'

## Appeal detail page (/dashboard/events/[id]) — brand redesign
- Sharp edges, gap-px grids, typography-as-hero
- Same link card component with share-first design
- Embedded leaderboard section
- Inline link creation (same as Collect)
- Clone appeal button
- Appeal details in collapsed <details> (context, not hero)
- Download all QR codes link
- Public progress page link

## Leaderboard page — brand redesign
- Total raised as hero number (dark section)
- Progress bars relative to leader
- Medal badges for top 3
- Conversion rate badges
- Auto-refresh every 10 seconds (live event mode)

## Route cleanup
- /dashboard/events re-exports /dashboard/collect (backward compat)
- Old events/page.tsx removed (was duplicate)

5 files changed, 3 pages redesigned
2026-03-04 21:13:32 +08:00

86 lines
3.6 KiB
PHP

<?php
namespace App\Filament\Widgets;
use App\Models\Donation;
use App\Models\ScheduledGivingDonation;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Database\Eloquent\Builder;
class DonationStatsWidget extends StatsOverviewWidget
{
protected static ?int $sort = 1;
protected int | string | array $columnSpan = 'full';
protected function getStats(): array
{
$confirmedScope = fn (Builder $q) => $q->whereNotNull('confirmed_at');
// Today
$todayQuery = Donation::whereHas('donationConfirmation', $confirmedScope)
->whereDate('created_at', today());
$todayCount = $todayQuery->count();
$todayRevenue = $todayQuery->sum('amount') / 100;
// This week
$weekQuery = Donation::whereHas('donationConfirmation', $confirmedScope)
->where('created_at', '>=', now()->startOfWeek());
$weekRevenue = $weekQuery->sum('amount') / 100;
// This month
$monthQuery = Donation::whereHas('donationConfirmation', $confirmedScope)
->where('created_at', '>=', now()->startOfMonth());
$monthCount = $monthQuery->count();
$monthRevenue = $monthQuery->sum('amount') / 100;
// Last month for trend
$lastMonthRevenue = Donation::whereHas('donationConfirmation', $confirmedScope)
->whereBetween('created_at', [now()->subMonth()->startOfMonth(), now()->subMonth()->endOfMonth()])
->sum('amount') / 100;
$monthTrend = $lastMonthRevenue > 0
? round(($monthRevenue - $lastMonthRevenue) / $lastMonthRevenue * 100, 1)
: null;
// Failed/incomplete donations today (people who tried but didn't complete)
$incompleteToday = Donation::whereDoesntHave('donationConfirmation', $confirmedScope)
->whereDate('created_at', today())
->count();
// Monthly supporters
$activeSG = ScheduledGivingDonation::where('is_active', true)->count();
// Zakat this month
$zakatMonth = Donation::whereHas('donationConfirmation', $confirmedScope)
->where('created_at', '>=', now()->startOfMonth())
->whereHas('donationPreferences', fn ($q) => $q->where('is_zakat', true))
->sum('amount') / 100;
return [
Stat::make("Today's Donations", '£' . number_format($todayRevenue, 0))
->description($todayCount . ' donations received' . ($incompleteToday > 0 ? " · {$incompleteToday} incomplete" : ''))
->descriptionIcon($incompleteToday > 0 ? 'heroicon-m-exclamation-triangle' : 'heroicon-m-check-circle')
->color($incompleteToday > 5 ? 'warning' : 'success'),
Stat::make('This Month', '£' . number_format($monthRevenue, 0))
->description(
$monthCount . ' donations' .
($monthTrend !== null ? ' · ' . ($monthTrend >= 0 ? '↑' : '↓') . abs($monthTrend) . '% vs last month' : '')
)
->descriptionIcon('heroicon-m-arrow-trending-up')
->color($monthTrend !== null && $monthTrend >= 0 ? 'success' : 'warning'),
Stat::make('Monthly Supporters', number_format($activeSG))
->description('People giving every month')
->descriptionIcon('heroicon-m-heart')
->color('success'),
Stat::make('Zakat This Month', '£' . number_format($zakatMonth, 0))
->description('Zakat-eligible donations')
->descriptionIcon('heroicon-m-star')
->color('info'),
];
}
}