## New: Community Leader role Who: Imam Yusuf, Sister Mariam, Uncle Tariq — the person who rallies their mosque, WhatsApp group, neighbourhood to pledge. Not an admin. Not a volunteer. A logged-in coordinator who needs more than a live feed but less than full admin access. /dashboard/community — their scoped dashboard: - 'How are WE doing?' — their stats vs the whole appeal (dark hero section) - Contribution percentage bar - Their links with full share buttons (Copy/WhatsApp/Email/QR) - Create new links (auto-tagged with their name) - Leaderboard: 'How communities compare' with 'You' badge - Read-only pledge list (no status changes, no bank details) Navigation changes for community_leader role: - Sees: My Community → Share Links → Reports (3 items) - Does NOT see: Home, Money, Settings, New Appeal button - Does NOT see: Bank details, WhatsApp config, reconciliation ## New: Team management API + UI GET/POST/PATCH/DELETE /api/team — CRUD for team members - Only org_admin/super_admin can invite - Temp password generated on invite (shown once) - Copy credentials or send via WhatsApp button - Role selector with descriptions (Admin, Community Leader, Staff, Volunteer) - Role change via dropdown, remove with trash icon - Can't change own role or remove self ## Settings page redesign Reordered by Aaisha's thinking: 1. WhatsApp (unchanged — most important) 2. Team (NEW — 'who has access? invite community leaders') 3. Bank account 4. Charity details 5. Direct Debit (collapsed in <details>) Team section shows: - All members with role icons (Crown/Users/Eye) - Inline role change dropdown - Remove button - Invite form with role cards and descriptions - Credentials shown once with copy + WhatsApp share buttons ## Admin page redesign Brand-consistent: no more shadcn Card/Badge/Table - Dark hero section with 7 platform stats - Pipeline status breakdown (gap-px grid) - Pill tab switcher (not shadcn Tabs) - Grid tables matching the rest of the dashboard - Role badges color-coded (blue super, green admin, amber leader) 6 files changed, 4 new routes/pages
128 lines
5.3 KiB
PHP
128 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Widgets;
|
|
|
|
use App\Filament\Resources\AppealResource;
|
|
use App\Models\Appeal;
|
|
use Filament\Tables\Actions\Action;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Table;
|
|
use Filament\Widgets\TableWidget as BaseWidget;
|
|
|
|
/**
|
|
* Shows fundraisers that need staff attention right now.
|
|
*
|
|
* This is the "nurture queue" — it surfaces fundraisers that are
|
|
* stalling, almost succeeding, or brand new, so Jasmine can
|
|
* proactively help supporters succeed.
|
|
*/
|
|
class FundraiserNurtureWidget extends BaseWidget
|
|
{
|
|
protected static ?int $sort = 5;
|
|
|
|
protected int | string | array $columnSpan = 'full';
|
|
|
|
protected static ?string $heading = 'Fundraisers That Need Your Help';
|
|
|
|
protected int $defaultPaginationPageOption = 5;
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->query(
|
|
Appeal::query()
|
|
->where('status', 'confirmed')
|
|
->where('is_accepting_donations', true)
|
|
->where(function ($q) {
|
|
$q->where(function ($q2) {
|
|
// Needs outreach: £0 raised, 7+ days old
|
|
$q2->where('amount_raised', 0)
|
|
->where('created_at', '<', now()->subDays(7))
|
|
->where('created_at', '>', now()->subDays(90)); // Not ancient
|
|
})
|
|
->orWhere(function ($q2) {
|
|
// Almost there: 80%+ of target
|
|
$q2->where('amount_raised', '>', 0)
|
|
->whereRaw('amount_raised >= amount_to_raise * 0.8')
|
|
->whereRaw('amount_raised < amount_to_raise');
|
|
})
|
|
->orWhere(function ($q2) {
|
|
// New this week
|
|
$q2->where('created_at', '>=', now()->subDays(7));
|
|
});
|
|
})
|
|
->with('user')
|
|
->orderByRaw("
|
|
CASE
|
|
WHEN amount_raised > 0 AND amount_raised >= amount_to_raise * 0.8 AND amount_raised < amount_to_raise THEN 1
|
|
WHEN created_at >= NOW() - INTERVAL '7 days' THEN 2
|
|
WHEN amount_raised = 0 AND created_at < NOW() - INTERVAL '7 days' THEN 3
|
|
ELSE 4
|
|
END ASC
|
|
")
|
|
)
|
|
->columns([
|
|
TextColumn::make('priority')
|
|
->label('')
|
|
->getStateUsing(function (Appeal $a) {
|
|
$raised = $a->amount_raised;
|
|
$target = $a->amount_to_raise;
|
|
$age = $a->created_at?->diffInDays(now()) ?? 0;
|
|
|
|
if ($raised > 0 && $raised >= $target * 0.8 && $raised < $target) return '🟡 Almost there';
|
|
if ($age <= 7) return '🆕 New';
|
|
if ($raised == 0) return '🔴 Needs help';
|
|
return '—';
|
|
})
|
|
->badge()
|
|
->color(function (Appeal $a) {
|
|
$raised = $a->amount_raised;
|
|
$target = $a->amount_to_raise;
|
|
$age = $a->created_at?->diffInDays(now()) ?? 0;
|
|
|
|
if ($raised > 0 && $raised >= $target * 0.8) return 'warning';
|
|
if ($age <= 7) return 'info';
|
|
return 'danger';
|
|
}),
|
|
|
|
TextColumn::make('name')
|
|
->label('Fundraiser')
|
|
->limit(35)
|
|
->weight('bold')
|
|
->description(fn (Appeal $a) => $a->user?->name ? 'by ' . $a->user->name : ''),
|
|
|
|
TextColumn::make('progress')
|
|
->label('Progress')
|
|
->getStateUsing(function (Appeal $a) {
|
|
$raised = $a->amount_raised / 100;
|
|
$target = $a->amount_to_raise / 100;
|
|
$pct = $target > 0 ? round($raised / $target * 100) : 0;
|
|
return '£' . number_format($raised, 0) . ' / £' . number_format($target, 0) . " ({$pct}%)";
|
|
}),
|
|
|
|
TextColumn::make('created_at')
|
|
->label('Created')
|
|
->since(),
|
|
])
|
|
->actions([
|
|
Action::make('view')
|
|
->label('Open')
|
|
->icon('heroicon-o-arrow-right')
|
|
->url(fn (Appeal $a) => AppealResource::getUrl('edit', ['record' => $a]))
|
|
->color('gray'),
|
|
|
|
Action::make('email')
|
|
->label('Email Owner')
|
|
->icon('heroicon-o-envelope')
|
|
->url(fn (Appeal $a) => $a->user?->email ? 'mailto:' . $a->user->email : null)
|
|
->visible(fn (Appeal $a) => (bool) $a->user?->email)
|
|
->openUrlInNewTab()
|
|
->color('info'),
|
|
])
|
|
->paginated([5, 10])
|
|
->emptyStateHeading('All fundraisers are doing well!')
|
|
->emptyStateDescription('No fundraisers need attention right now.')
|
|
->emptyStateIcon('heroicon-o-face-smile');
|
|
}
|
|
}
|