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:
429
temp_files/v3/AppealResource.php
Normal file
429
temp_files/v3/AppealResource.php
Normal file
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\AppealResource\Pages\EditAppeal;
|
||||
use App\Filament\Resources\AppealResource\Pages\ListAppeals;
|
||||
use App\Filament\Resources\AppealResource\RelationManagers\AppealChildrenRelationManager;
|
||||
use App\Filament\Resources\AppealResource\RelationManagers\AppealDonationsRelationManager;
|
||||
use App\Jobs\N3O\Data\SendAppeal;
|
||||
use App\Jobs\WordPress\SyncAppeal;
|
||||
use App\Models\Appeal;
|
||||
use App\Models\DonationType;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\GlobalSearch\Actions\Action as GlobalSearchAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Enums\FiltersLayout;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class AppealResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Appeal::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-hand-raised';
|
||||
|
||||
protected static ?string $navigationGroup = 'Campaigns';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static ?string $modelLabel = 'Fundraiser';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Fundraisers';
|
||||
|
||||
protected static ?string $navigationLabel = 'Fundraisers';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
// ─── Global Search ────────────────────────────────────────────
|
||||
|
||||
public static function getGloballySearchableAttributes(): array
|
||||
{
|
||||
return ['name', 'slug', 'description'];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultTitle(Model $record): string|HtmlString
|
||||
{
|
||||
return $record->name;
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultDetails(Model $record): array
|
||||
{
|
||||
$pct = $record->amount_to_raise > 0
|
||||
? round($record->amount_raised / $record->amount_to_raise * 100)
|
||||
: 0;
|
||||
|
||||
return [
|
||||
'Progress' => '£' . number_format($record->amount_raised / 100, 0) . ' / £' . number_format($record->amount_to_raise / 100, 0) . " ({$pct}%)",
|
||||
'Status' => $record->is_accepting_donations ? 'Live' : 'Closed',
|
||||
'Owner' => $record->user?->name ?? '—',
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultActions(Model $record): array
|
||||
{
|
||||
return [
|
||||
GlobalSearchAction::make('edit')
|
||||
->label('Open Fundraiser')
|
||||
->url(static::getUrl('edit', ['record' => $record])),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getGlobalSearchEloquentQuery()
|
||||
->with('user')
|
||||
->latest('created_at');
|
||||
}
|
||||
|
||||
// ─── Form ────────────────────────────────────────────────────
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Fieldset::make('Connections')->schema([
|
||||
Select::make('parent_appeal_id')
|
||||
->label('Parent Fundraiser')
|
||||
->relationship('parent', 'name', modifyQueryUsing: fn ($query) => $query->whereNull('parent_appeal_id'))
|
||||
->required(false)
|
||||
->searchable(),
|
||||
|
||||
Select::make('user_id')
|
||||
->label('Page Owner')
|
||||
->searchable()
|
||||
->relationship('user', 'name')
|
||||
->required(),
|
||||
]),
|
||||
|
||||
Section::make('General Info')->schema([
|
||||
Fieldset::make('Info')->schema([
|
||||
TextInput::make('name')
|
||||
->label('Fundraiser Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('slug')
|
||||
->label('URL Slug')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(
|
||||
table: 'appeals',
|
||||
column: 'slug',
|
||||
ignorable: $form->getRecord(),
|
||||
modifyRuleUsing: fn ($rule) => $rule->whereNull('deleted_at')
|
||||
)
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('description')
|
||||
->label('Short Description')
|
||||
->required()
|
||||
->maxLength(512),
|
||||
]),
|
||||
|
||||
Fieldset::make('Target & Allocation')->schema([
|
||||
TextInput::make('amount_to_raise')
|
||||
->label('Fundraising Target')
|
||||
->numeric()
|
||||
->minValue(150)
|
||||
->maxValue(999_999_999)
|
||||
->prefix('£')
|
||||
->required(),
|
||||
|
||||
TextInput::make('amount_raised')
|
||||
->label('Amount Raised So Far')
|
||||
->numeric()
|
||||
->prefix('£')
|
||||
->disabled(),
|
||||
|
||||
Select::make('donation_type_id')
|
||||
->label('Cause')
|
||||
->relationship('donationType', 'display_name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->live()
|
||||
->required(),
|
||||
|
||||
Select::make('donation_country_id')
|
||||
->label('Country')
|
||||
->required()
|
||||
->visible(function (\Filament\Forms\Get $get) {
|
||||
$donationTypeId = $get('donation_type_id');
|
||||
if (!($donationType = DonationType::find($donationTypeId))) {
|
||||
return false;
|
||||
}
|
||||
return $donationType->donationCountries()->count() > 1;
|
||||
})
|
||||
->options(function (\Filament\Forms\Get $get) {
|
||||
$donationTypeId = $get('donation_type_id');
|
||||
if (!($donationType = DonationType::find($donationTypeId))) {
|
||||
return [];
|
||||
}
|
||||
return $donationType->donationCountries->pluck('name', 'id')->toArray();
|
||||
})
|
||||
->live(),
|
||||
]),
|
||||
|
||||
Fieldset::make('Settings')->schema([
|
||||
Toggle::make('is_visible')
|
||||
->label('Visible on website'),
|
||||
|
||||
Toggle::make('is_in_memory')
|
||||
->label('In memory of someone')
|
||||
->live(),
|
||||
|
||||
TextInput::make('in_memory_name')
|
||||
->label('In memory of')
|
||||
->required(fn (\Filament\Forms\Get $get) => $get('is_in_memory'))
|
||||
->visible(fn (\Filament\Forms\Get $get) => $get('is_in_memory')),
|
||||
|
||||
Toggle::make('is_accepting_donations')
|
||||
->label('Accepting donations'),
|
||||
|
||||
Toggle::make('is_team_campaign')
|
||||
->label('Team fundraiser'),
|
||||
|
||||
Toggle::make('is_accepting_members')
|
||||
->label('Accepting team members')
|
||||
->live()
|
||||
->visible(fn (\Filament\Forms\Get $get) => $get('is_team_campaign')),
|
||||
]),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
|
||||
Section::make('Content')->schema([
|
||||
FileUpload::make('picture')
|
||||
->label('Cover Image')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
|
||||
RichEditor::make('story')
|
||||
->label('Fundraiser Story')
|
||||
->required()
|
||||
->minLength(150)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Table ────────────────────────────────────────────────────
|
||||
// Designed for the "Fundraiser Nurture" journey:
|
||||
// Jasmine opens this page and immediately sees which fundraisers
|
||||
// need attention, which are succeeding, which are stale.
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label('Fundraiser')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->weight('bold')
|
||||
->limit(40)
|
||||
->description(fn (Appeal $a) => $a->user?->name ? 'by ' . $a->user->name : null)
|
||||
->tooltip(fn (Appeal $a) => $a->name),
|
||||
|
||||
TextColumn::make('status_label')
|
||||
->label('Status')
|
||||
->getStateUsing(function (Appeal $a) {
|
||||
if ($a->status === 'pending') return 'Pending Review';
|
||||
if (!$a->is_accepting_donations) return 'Closed';
|
||||
return 'Live';
|
||||
})
|
||||
->badge()
|
||||
->color(fn ($state) => match ($state) {
|
||||
'Live' => 'success',
|
||||
'Pending Review' => 'warning',
|
||||
'Closed' => 'gray',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
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}%)";
|
||||
})
|
||||
->color(function (Appeal $a) {
|
||||
$pct = $a->amount_to_raise > 0 ? $a->amount_raised / $a->amount_to_raise : 0;
|
||||
if ($pct >= 1) return 'success';
|
||||
if ($pct >= 0.5) return 'info';
|
||||
if ($pct > 0) return 'warning';
|
||||
return 'danger';
|
||||
})
|
||||
->weight('bold'),
|
||||
|
||||
TextColumn::make('donationType.display_name')
|
||||
->label('Cause')
|
||||
->badge()
|
||||
->color('info')
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('nurture_status')
|
||||
->label('Needs Attention?')
|
||||
->getStateUsing(function (Appeal $a) {
|
||||
if ($a->status !== 'confirmed') return null;
|
||||
if (!$a->is_accepting_donations) return null;
|
||||
|
||||
$raised = $a->amount_raised;
|
||||
$target = $a->amount_to_raise;
|
||||
$age = $a->created_at?->diffInDays(now()) ?? 0;
|
||||
|
||||
if ($raised == 0 && $age > 7) return '🔴 No donations yet';
|
||||
if ($raised > 0 && $raised >= $target * 0.8 && $raised < $target) return '🟡 Almost there!';
|
||||
if ($raised >= $target) return '🟢 Target reached!';
|
||||
if ($raised > 0 && $age > 30) return '🟠 Slowing down';
|
||||
|
||||
return null;
|
||||
})
|
||||
->placeholder('—')
|
||||
->wrap(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable()
|
||||
->description(fn (Appeal $a) => $a->created_at?->format('d M Y')),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('nurture_segment')
|
||||
->label('Nurture Segment')
|
||||
->options([
|
||||
'needs_outreach' => '🔴 Needs Outreach (£0 raised, 7+ days)',
|
||||
'almost_there' => '🟡 Almost There (80%+ of target)',
|
||||
'target_reached' => '🟢 Target Reached',
|
||||
'slowing' => '🟠 Slowing Down (raised something, 30+ days)',
|
||||
'new_this_week' => '🆕 New This Week',
|
||||
])
|
||||
->query(function (Builder $query, array $data) {
|
||||
if (!$data['value']) return;
|
||||
$query->where('status', 'confirmed')->where('is_accepting_donations', true);
|
||||
|
||||
match ($data['value']) {
|
||||
'needs_outreach' => $query->where('amount_raised', 0)->where('created_at', '<', now()->subDays(7)),
|
||||
'almost_there' => $query->where('amount_raised', '>', 0)->whereRaw('amount_raised >= amount_to_raise * 0.8')->whereRaw('amount_raised < amount_to_raise'),
|
||||
'target_reached' => $query->whereRaw('amount_raised >= amount_to_raise')->where('amount_raised', '>', 0),
|
||||
'slowing' => $query->where('amount_raised', '>', 0)->whereRaw('amount_raised < amount_to_raise * 0.8')->where('created_at', '<', now()->subDays(30)),
|
||||
'new_this_week' => $query->where('created_at', '>=', now()->subDays(7)),
|
||||
default => null,
|
||||
};
|
||||
}),
|
||||
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'confirmed' => 'Live',
|
||||
'pending' => 'Pending Review',
|
||||
]),
|
||||
|
||||
Filter::make('accepting_donations')
|
||||
->label('Currently accepting donations')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->where('is_accepting_donations', true))
|
||||
->default(),
|
||||
|
||||
Filter::make('has_raised')
|
||||
->label('Has raised money')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->where('amount_raised', '>', 0)),
|
||||
], layout: FiltersLayout::AboveContentCollapsible)
|
||||
->actions([
|
||||
Action::make('view_page')
|
||||
->label('View Page')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
->url(fn (Appeal $a) => 'https://www.charityright.org.uk/fundraiser/' . $a->slug)
|
||||
->openUrlInNewTab(),
|
||||
|
||||
ActionGroup::make([
|
||||
EditAction::make(),
|
||||
|
||||
Action::make('email_owner')
|
||||
->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(),
|
||||
|
||||
Action::make('send_to_engage')
|
||||
->label('Send to Engage')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->action(function (Appeal $appeal) {
|
||||
dispatch(new SendAppeal($appeal));
|
||||
Notification::make()->title('Sent to Engage')->success()->send();
|
||||
}),
|
||||
|
||||
Action::make('appeal_owner')
|
||||
->label('Page Owner Details')
|
||||
->icon('heroicon-o-user')
|
||||
->modalContent(fn (Appeal $appeal): View => view('filament.fields.appeal-owner', ['appeal' => $appeal]))
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelAction(false),
|
||||
]),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkAction::make('send_to_engage')
|
||||
->label('Send to Engage')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->action(function ($records) {
|
||||
foreach ($records as $appeal) {
|
||||
dispatch(new SendAppeal($appeal));
|
||||
}
|
||||
Notification::make()->title(count($records) . ' sent to Engage')->success()->send();
|
||||
}),
|
||||
|
||||
BulkAction::make('send_to_wp')
|
||||
->label('Sync to WordPress')
|
||||
->icon('heroicon-o-globe-alt')
|
||||
->action(function ($records) {
|
||||
foreach ($records as $appeal) {
|
||||
dispatch(new SyncAppeal($appeal));
|
||||
}
|
||||
Notification::make()->title(count($records) . ' synced to WordPress')->success()->send();
|
||||
}),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->searchPlaceholder('Search by fundraiser name...');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
AppealDonationsRelationManager::class,
|
||||
AppealChildrenRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAppeals::route('/'),
|
||||
'edit' => EditAppeal::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
493
temp_files/v3/DonationResource.php
Normal file
493
temp_files/v3/DonationResource.php
Normal file
@@ -0,0 +1,493 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Definitions\DonationOccurrence;
|
||||
use App\Filament\Exports\DonationExporter;
|
||||
use App\Filament\Resources\DonationResource\Pages\EditDonation;
|
||||
use App\Filament\Resources\DonationResource\Pages\ListDonations;
|
||||
use App\Filament\Resources\DonationResource\RelationManagers\EventLogsRelationManager;
|
||||
use App\Filament\RelationManagers\InternalNotesRelationManager;
|
||||
use App\Helpers;
|
||||
use App\Jobs\N3O\Data\SendDonation;
|
||||
use App\Jobs\Zapier\Data\SendCustomer;
|
||||
use App\Mail\DonationConfirmed;
|
||||
use App\Models\Donation;
|
||||
use App\Models\DonationCountry;
|
||||
use App\Models\DonationType;
|
||||
use App\Models\EverGiveDonation;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\GlobalSearch\Actions\Action as GlobalSearchAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\ExportAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Enums\FiltersLayout;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Throwable;
|
||||
|
||||
class DonationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Donation::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
|
||||
|
||||
protected static ?string $navigationGroup = 'Donations';
|
||||
|
||||
protected static ?string $navigationLabel = 'All Donations';
|
||||
|
||||
protected static ?string $modelLabel = 'Donation';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Donations';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
// ─── Global Search ────────────────────────────────────────────
|
||||
// Staff search by donor name, email, or payment reference.
|
||||
// "Someone called about payment ref pi_3xxx" → finds it instantly.
|
||||
|
||||
public static function getGloballySearchableAttributes(): array
|
||||
{
|
||||
return ['reference_code', 'provider_reference'];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultTitle(Model $record): string|HtmlString
|
||||
{
|
||||
$status = $record->isConfirmed() ? '✓' : '✗';
|
||||
return $status . ' £' . number_format($record->amount / 100, 2) . ' — ' . ($record->customer?->name ?? 'Unknown donor');
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultDetails(Model $record): array
|
||||
{
|
||||
return [
|
||||
'Cause' => $record->donationType?->display_name ?? '—',
|
||||
'Date' => $record->created_at?->format('d M Y H:i'),
|
||||
'Ref' => $record->reference_code ?? '—',
|
||||
'Status' => $record->isConfirmed() ? 'Confirmed' : 'Incomplete',
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultActions(Model $record): array
|
||||
{
|
||||
return [
|
||||
GlobalSearchAction::make('edit')
|
||||
->label('View Donation')
|
||||
->url(static::getUrl('edit', ['record' => $record])),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getGlobalSearchEloquentQuery()
|
||||
->with(['customer', 'donationType', 'donationConfirmation'])
|
||||
->latest('created_at');
|
||||
}
|
||||
|
||||
public static function getGlobalSearchResultsLimit(): int
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
|
||||
// ─── Form (Edit/View screen) ─────────────────────────────────
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Donation Details')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Grid::make(5)->schema([
|
||||
Placeholder::make('Confirmed?')
|
||||
->content(fn (Donation $record) => new HtmlString(
|
||||
$record->isConfirmed()
|
||||
? '<span class="text-green-600 font-bold">✓ Confirmed</span>'
|
||||
: '<span class="text-red-600 font-bold">✗ Incomplete</span>'
|
||||
)),
|
||||
|
||||
Placeholder::make('Amount')
|
||||
->content(fn (Donation $record) => new HtmlString('<b>' . Helpers::formatMoneyGlobal($record->donationTotal()) . '</b>')),
|
||||
|
||||
Placeholder::make('Admin Contribution')
|
||||
->content(fn (Donation $record) => new HtmlString('<b>' . Helpers::formatMoneyGlobal($record->donationAdminAmount()) . '</b>')),
|
||||
|
||||
Placeholder::make('Gift Aid?')
|
||||
->content(fn (Donation $record) => new HtmlString(
|
||||
$record->isGiftAid()
|
||||
? '<span class="text-green-600 font-bold">✓ Yes</span>'
|
||||
: '<span class="text-gray-400">No</span>'
|
||||
)),
|
||||
|
||||
Placeholder::make('Zakat?')
|
||||
->content(fn (Donation $record) => new HtmlString(
|
||||
$record->isZakat()
|
||||
? '<span class="text-green-600 font-bold">✓ Yes</span>'
|
||||
: '<span class="text-gray-400">No</span>'
|
||||
)),
|
||||
]),
|
||||
|
||||
Fieldset::make('Donor')
|
||||
->columns(3)
|
||||
->schema([
|
||||
Placeholder::make('name')
|
||||
->content(fn (Donation $donation) => $donation->customer?->name ?? '—'),
|
||||
|
||||
Placeholder::make('email')
|
||||
->content(fn (Donation $donation) => $donation->customer?->email ?? '—'),
|
||||
|
||||
Placeholder::make('phone')
|
||||
->content(fn (Donation $donation) => strlen(trim($phone = $donation->customer?->phone ?? '')) > 0 ? $phone : new HtmlString('<i class="text-gray-400">Not provided</i>')),
|
||||
])
|
||||
->visible(fn (Donation $donation) => (bool) $donation->customer),
|
||||
|
||||
Fieldset::make('Address')
|
||||
->columns(3)
|
||||
->schema([
|
||||
Placeholder::make('house')
|
||||
->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->house ?? '')) ? $v : new HtmlString('<i class="text-gray-400">—</i>')),
|
||||
|
||||
Placeholder::make('street')
|
||||
->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->street ?? '')) ? $v : new HtmlString('<i class="text-gray-400">—</i>')),
|
||||
|
||||
Placeholder::make('town')
|
||||
->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->town ?? '')) ? $v : new HtmlString('<i class="text-gray-400">—</i>')),
|
||||
|
||||
Placeholder::make('state')
|
||||
->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->state ?? '')) ? $v : new HtmlString('<i class="text-gray-400">—</i>')),
|
||||
|
||||
Placeholder::make('postcode')
|
||||
->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->postcode ?? '')) ? $v : new HtmlString('<i class="text-gray-400">—</i>')),
|
||||
|
||||
Placeholder::make('country_code')
|
||||
->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->country_code ?? '')) ? $v : new HtmlString('<i class="text-gray-400">—</i>')),
|
||||
])
|
||||
->visible(fn (Donation $donation) => (bool) $donation->address),
|
||||
|
||||
Fieldset::make('Allocation')
|
||||
->schema([
|
||||
Select::make('donation_type_id')
|
||||
->label('Cause')
|
||||
->relationship('donationType', 'display_name')
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
Select::make('donation_country_id')
|
||||
->label('Country')
|
||||
->relationship('donationCountry', 'name')
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
Select::make('appeal_id')
|
||||
->label('Fundraiser')
|
||||
->relationship('appeal', 'name')
|
||||
->searchable()
|
||||
->live()
|
||||
->disabled()
|
||||
->visible(fn (Donation $donation) => $donation->appeal_id !== null),
|
||||
|
||||
Placeholder::make('ever_give_donation')
|
||||
->content(fn (Donation $donation) =>
|
||||
'£' . number_format(
|
||||
EverGiveDonation::where('donation_id', $donation->id)->value('amount') ?? 0,
|
||||
2
|
||||
)
|
||||
)->visible(function (Donation $donation) {
|
||||
return EverGiveDonation::where('donation_id', $donation->id)->where('status', '!=', 'failed_to_send_to_ever_give_api')->first();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Table ────────────────────────────────────────────────────
|
||||
// Designed for two workflows:
|
||||
// 1. "Find a specific donation" (search by donor name/email/ref)
|
||||
// 2. "What happened today?" (default sort, quick status scan)
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
IconColumn::make('is_confirmed')
|
||||
->label('')
|
||||
->boolean()
|
||||
->trueIcon('heroicon-o-check-circle')
|
||||
->falseIcon('heroicon-o-x-circle')
|
||||
->trueColor('success')
|
||||
->falseColor('danger')
|
||||
->tooltip(fn (Donation $d) => $d->isConfirmed() ? 'Payment confirmed' : 'Payment incomplete — may need follow-up'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Date')
|
||||
->dateTime('d M Y H:i')
|
||||
->sortable()
|
||||
->description(fn (Donation $d) => $d->created_at?->diffForHumans()),
|
||||
|
||||
TextColumn::make('customer.name')
|
||||
->label('Donor')
|
||||
->description(fn (Donation $d) => $d->customer?->email)
|
||||
->searchable(query: function (Builder $query, string $search) {
|
||||
$query->whereHas('customer', fn ($q) => $q
|
||||
->where('first_name', 'like', "%{$search}%")
|
||||
->orWhere('last_name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
);
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('amount')
|
||||
->label('Amount')
|
||||
->money('gbp', divideBy: 100)
|
||||
->sortable()
|
||||
->weight('bold'),
|
||||
|
||||
TextColumn::make('donationType.display_name')
|
||||
->label('Cause')
|
||||
->badge()
|
||||
->color('info')
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('reoccurrence')
|
||||
->label('Type')
|
||||
->formatStateUsing(fn ($state) => match ((int) $state) {
|
||||
-1 => 'One-off',
|
||||
2 => 'Monthly',
|
||||
default => DonationOccurrence::translate($state),
|
||||
})
|
||||
->badge()
|
||||
->color(fn ($state) => match ((int) $state) {
|
||||
-1 => 'gray',
|
||||
2 => 'success',
|
||||
default => 'warning',
|
||||
}),
|
||||
|
||||
TextColumn::make('appeal.name')
|
||||
->label('Fundraiser')
|
||||
->toggleable()
|
||||
->limit(25)
|
||||
->placeholder('Direct'),
|
||||
|
||||
TextColumn::make('reference_code')
|
||||
->label('Ref')
|
||||
->searchable()
|
||||
->fontFamily('mono')
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('provider_reference')
|
||||
->label('Payment Ref')
|
||||
->searchable()
|
||||
->fontFamily('mono')
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters(static::getTableFilters(), layout: FiltersLayout::AboveContentCollapsible)
|
||||
->actions(static::getTableRowActions())
|
||||
->bulkActions(static::getBulkActions())
|
||||
->headerActions([
|
||||
ExportAction::make('donations')
|
||||
->visible(fn () => Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation'))
|
||||
->label('Export')
|
||||
->icon('heroicon-o-arrow-down-on-square')
|
||||
->exporter(DonationExporter::class),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->searchPlaceholder('Search by donor name, email, or reference...')
|
||||
->poll('30s');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
EventLogsRelationManager::class,
|
||||
InternalNotesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListDonations::route('/'),
|
||||
'edit' => EditDonation::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Table Actions ───────────────────────────────────────────
|
||||
// Quick actions that solve problems without navigating away.
|
||||
// "See a failed donation? → Resend receipt. See unknown donor? → Open profile."
|
||||
|
||||
private static function getTableRowActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('view_donor')
|
||||
->label('Donor')
|
||||
->icon('heroicon-o-user')
|
||||
->color('gray')
|
||||
->url(fn (Donation $d) => $d->customer_id
|
||||
? CustomerResource::getUrl('edit', ['record' => $d->customer_id])
|
||||
: null)
|
||||
->visible(fn (Donation $d) => (bool) $d->customer_id)
|
||||
->openUrlInNewTab(),
|
||||
|
||||
ActionGroup::make([
|
||||
ViewAction::make(),
|
||||
|
||||
Action::make('send_receipt')
|
||||
->label('Send Receipt')
|
||||
->icon('heroicon-o-envelope')
|
||||
->requiresConfirmation()
|
||||
->modalDescription(fn (Donation $d) => 'Send receipt to ' . ($d->customer?->email ?? '?'))
|
||||
->visible(fn (Donation $d) => $d->isConfirmed() && (Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation')))
|
||||
->action(function (Donation $donation) {
|
||||
try {
|
||||
Mail::to($donation->customer->email)->send(new DonationConfirmed($donation));
|
||||
Notification::make()->title('Receipt sent to ' . $donation->customer->email)->success()->send();
|
||||
} catch (Throwable $e) {
|
||||
Log::error($e);
|
||||
Notification::make()->title('Failed to send receipt')->body($e->getMessage())->danger()->send();
|
||||
}
|
||||
}),
|
||||
|
||||
Action::make('view_in_stripe')
|
||||
->label('View in Stripe')
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->visible(fn (Donation $d) => (bool) $d->provider_reference)
|
||||
->url(fn (Donation $d) => 'https://dashboard.stripe.com/search?query=' . urlencode($d->provider_reference))
|
||||
->openUrlInNewTab(),
|
||||
|
||||
Action::make('send_to_engage')
|
||||
->label('Send to Engage')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->visible(fn (Donation $d) => $d->isConfirmed() && (Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation')))
|
||||
->action(function (Donation $donation) {
|
||||
dispatch(new SendDonation($donation));
|
||||
Notification::make()->title('Sent to Engage')->success()->send();
|
||||
}),
|
||||
|
||||
Action::make('send_to_zapier')
|
||||
->label('Send to Zapier')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->visible(fn (Donation $d) => $d->isConfirmed() && (Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation')))
|
||||
->action(function (Donation $donation) {
|
||||
dispatch(new SendCustomer($donation->customer));
|
||||
Notification::make()->title('Sent to Zapier')->success()->send();
|
||||
}),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private static function getBulkActions()
|
||||
{
|
||||
return BulkActionGroup::make([
|
||||
BulkAction::make('send_receipt')
|
||||
->label('Send Receipts')
|
||||
->icon('heroicon-o-envelope')
|
||||
->requiresConfirmation()
|
||||
->visible(fn () => Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation'))
|
||||
->action(function ($records) {
|
||||
$sent = 0;
|
||||
foreach ($records as $donation) {
|
||||
try {
|
||||
Mail::to($donation->customer->email)->send(new DonationConfirmed($donation));
|
||||
$sent++;
|
||||
} catch (Throwable $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
Notification::make()->title($sent . ' receipts sent')->success()->send();
|
||||
}),
|
||||
|
||||
BulkAction::make('send_to_engage')
|
||||
->label('Send to Engage')
|
||||
->icon('heroicon-o-arrow-up-on-square')
|
||||
->visible(fn () => Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation'))
|
||||
->action(function ($records) {
|
||||
foreach ($records as $donation) {
|
||||
dispatch(new SendDonation($donation));
|
||||
}
|
||||
Notification::make()->title(count($records) . ' sent to Engage')->success()->send();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Filters ─────────────────────────────────────────────────
|
||||
// Designed around real questions:
|
||||
// "Show me today's incomplete donations" (investigating failures)
|
||||
// "Show me Zakat donations this month" (reporting)
|
||||
// "Show me donations to a specific cause" (allocation check)
|
||||
|
||||
private static function getTableFilters(): array
|
||||
{
|
||||
return [
|
||||
TernaryFilter::make('confirmed')
|
||||
->label('Payment Status')
|
||||
->placeholder('All')
|
||||
->trueLabel('Confirmed only')
|
||||
->falseLabel('Incomplete only')
|
||||
->queries(
|
||||
true: fn (Builder $q) => $q->whereHas('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')),
|
||||
false: fn (Builder $q) => $q->whereDoesntHave('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')),
|
||||
blank: fn (Builder $q) => $q,
|
||||
)
|
||||
->default(true),
|
||||
|
||||
SelectFilter::make('donation_type_id')
|
||||
->label('Cause')
|
||||
->options(fn () => DonationType::orderBy('display_name')->pluck('display_name', 'id'))
|
||||
->searchable(),
|
||||
|
||||
SelectFilter::make('donation_country_id')
|
||||
->label('Country')
|
||||
->options(fn () => DonationCountry::orderBy('name')->pluck('name', 'id'))
|
||||
->searchable(),
|
||||
|
||||
Filter::make('date_range')
|
||||
->form([
|
||||
DatePicker::make('from')->label('From'),
|
||||
DatePicker::make('to')->label('To'),
|
||||
])
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
return $query
|
||||
->when($data['from'], fn (Builder $q) => $q->whereDate('created_at', '>=', $data['from']))
|
||||
->when($data['to'], fn (Builder $q) => $q->whereDate('created_at', '<=', $data['to']));
|
||||
})
|
||||
->columns(2),
|
||||
|
||||
Filter::make('is_zakat')
|
||||
->label('Zakat')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas('donationPreferences', fn ($q2) => $q2->where('is_zakat', true))),
|
||||
|
||||
Filter::make('is_gift_aid')
|
||||
->label('Gift Aid')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas('donationPreferences', fn ($q2) => $q2->where('is_gift_aid', true))),
|
||||
|
||||
Filter::make('has_fundraiser')
|
||||
->label('Via Fundraiser')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereNotNull('appeal_id')),
|
||||
];
|
||||
}
|
||||
}
|
||||
127
temp_files/v3/FundraiserNurtureWidget.php
Normal file
127
temp_files/v3/FundraiserNurtureWidget.php
Normal 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');
|
||||
}
|
||||
}
|
||||
40
temp_files/v3/ListAppeals.php
Normal file
40
temp_files/v3/ListAppeals.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\AppealResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AppealResource;
|
||||
use App\Models\Appeal;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAppeals extends ListRecords
|
||||
{
|
||||
protected static string $resource = AppealResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'All Fundraisers';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$live = Appeal::where('status', 'confirmed')->where('is_accepting_donations', true)->count();
|
||||
$needsHelp = Appeal::where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
->where('amount_raised', 0)
|
||||
->where('created_at', '<', now()->subDays(7))
|
||||
->where('created_at', '>', now()->subDays(90))
|
||||
->count();
|
||||
$almostThere = Appeal::where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
->where('amount_raised', '>', 0)
|
||||
->whereRaw('amount_raised >= amount_to_raise * 0.8')
|
||||
->whereRaw('amount_raised < amount_to_raise')
|
||||
->count();
|
||||
|
||||
$parts = ["{$live} live fundraisers"];
|
||||
if ($needsHelp > 0) $parts[] = "{$needsHelp} need outreach";
|
||||
if ($almostThere > 0) $parts[] = "{$almostThere} almost at target";
|
||||
|
||||
return implode(' · ', $parts) . '. Use the "Nurture Segment" filter to find fundraisers that need your help.';
|
||||
}
|
||||
}
|
||||
40
temp_files/v3/ListDonations.php
Normal file
40
temp_files/v3/ListDonations.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DonationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DonationResource;
|
||||
use App\Models\Donation;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDonations extends ListRecords
|
||||
{
|
||||
protected static string $resource = DonationResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'All Donations';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$todayConfirmed = Donation::whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereDate('created_at', today())
|
||||
->count();
|
||||
$todayAmount = Donation::whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereDate('created_at', today())
|
||||
->sum('amount') / 100;
|
||||
$todayIncomplete = Donation::whereDoesntHave('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereDate('created_at', today())
|
||||
->count();
|
||||
|
||||
$parts = [
|
||||
"Today: {$todayConfirmed} confirmed (£" . number_format($todayAmount, 0) . ")",
|
||||
];
|
||||
|
||||
if ($todayIncomplete > 0) {
|
||||
$parts[] = "{$todayIncomplete} incomplete";
|
||||
}
|
||||
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user