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
63 lines
2.7 KiB
PHP
63 lines
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Widgets;
|
|
|
|
use App\Models\ApprovalQueue;
|
|
use App\Models\Donation;
|
|
use App\Models\EventLog;
|
|
use App\Models\ScheduledGivingDonation;
|
|
use Filament\Widgets\StatsOverviewWidget;
|
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
|
|
|
class OperationalHealthWidget extends StatsOverviewWidget
|
|
{
|
|
protected static ?int $sort = 3;
|
|
|
|
protected int | string | array $columnSpan = 'full';
|
|
|
|
protected function getStats(): array
|
|
{
|
|
// Fundraisers waiting for review
|
|
$pendingReview = ApprovalQueue::where('status', 'pending')->count();
|
|
|
|
// Incomplete donations in last 7 days (people who tried to give but something went wrong)
|
|
$incomplete7d = Donation::whereDoesntHave('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
|
->where('created_at', '>=', now()->subDays(7))
|
|
->count();
|
|
|
|
// Regular giving that recently stopped
|
|
$cancelledSG = ScheduledGivingDonation::where('is_active', false)
|
|
->where('updated_at', '>=', now()->subDays(30))
|
|
->count();
|
|
|
|
// System errors
|
|
$errorsToday = EventLog::whereDate('created_at', today())
|
|
->whereIn('status', ['failed', 'exception'])
|
|
->count();
|
|
|
|
return [
|
|
Stat::make('Fundraisers to Review', $pendingReview)
|
|
->description($pendingReview > 0 ? 'People are waiting for approval' : 'All caught up!')
|
|
->descriptionIcon($pendingReview > 0 ? 'heroicon-m-clock' : 'heroicon-m-check-circle')
|
|
->color($pendingReview > 20 ? 'danger' : ($pendingReview > 0 ? 'warning' : 'success'))
|
|
->url(route('filament.admin.resources.approval-queues.index')),
|
|
|
|
Stat::make('Incomplete Donations', $incomplete7d)
|
|
->description('Last 7 days — people tried but payment didn\'t complete')
|
|
->descriptionIcon('heroicon-m-exclamation-triangle')
|
|
->color($incomplete7d > 50 ? 'danger' : ($incomplete7d > 0 ? 'warning' : 'success')),
|
|
|
|
Stat::make('Cancelled Subscriptions', $cancelledSG)
|
|
->description('Monthly supporters who stopped in last 30 days')
|
|
->descriptionIcon($cancelledSG > 0 ? 'heroicon-m-arrow-down' : 'heroicon-m-check-circle')
|
|
->color($cancelledSG > 5 ? 'warning' : 'success'),
|
|
|
|
Stat::make('System Problems', $errorsToday)
|
|
->description($errorsToday > 0 ? 'Errors today — check activity log' : 'No problems today')
|
|
->descriptionIcon($errorsToday > 0 ? 'heroicon-m-x-circle' : 'heroicon-m-check-circle')
|
|
->color($errorsToday > 0 ? 'danger' : 'success')
|
|
->url(route('filament.admin.resources.event-logs.index')),
|
|
];
|
|
}
|
|
}
|