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
86 lines
3.6 KiB
PHP
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'),
|
|
];
|
|
}
|
|
}
|