Settings + Admin redesign + Community Leader role

## 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
This commit is contained in:
2026-03-04 21:48:10 +08:00
parent 9c7990e05c
commit b771858280
10 changed files with 2113 additions and 242 deletions

View File

@@ -0,0 +1,127 @@
<?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');
}
}