Telepathic Collect: link-first, flattened hierarchy, embedded leaderboard
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
This commit is contained in:
50
temp_files/DonationChartWidget.php
Normal file
50
temp_files/DonationChartWidget.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Donation;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class DonationChartWidget extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Daily Donations — Last 30 Days';
|
||||
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected int | string | array $columnSpan = 'full';
|
||||
|
||||
protected static ?string $maxHeight = '220px';
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$confirmedScope = fn (Builder $q) => $q->whereNotNull('confirmed_at');
|
||||
$days = collect(range(29, 0))->map(fn ($i) => now()->subDays($i)->format('Y-m-d'));
|
||||
|
||||
$donations = Donation::whereHas('donationConfirmation', $confirmedScope)
|
||||
->where('created_at', '>=', now()->subDays(30)->startOfDay())
|
||||
->selectRaw('DATE(created_at) as date, SUM(amount) / 100 as total')
|
||||
->groupByRaw('DATE(created_at)')
|
||||
->pluck('total', 'date');
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Revenue (£)',
|
||||
'data' => $days->map(fn ($d) => round($donations[$d] ?? 0, 2))->toArray(),
|
||||
'borderColor' => '#10b981',
|
||||
'backgroundColor' => 'rgba(16, 185, 129, 0.1)',
|
||||
'fill' => true,
|
||||
'tension' => 0.3,
|
||||
],
|
||||
],
|
||||
'labels' => $days->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
}
|
||||
85
temp_files/DonationStatsWidget.php
Normal file
85
temp_files/DonationStatsWidget.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
127
temp_files/EditCustomer.php
Normal file
127
temp_files/EditCustomer.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CustomerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CustomerResource;
|
||||
use App\Helpers;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Donation;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Infolists\Components\Grid;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\RepeatableEntry;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class EditCustomer extends EditRecord
|
||||
{
|
||||
protected static string $resource = CustomerResource::class;
|
||||
|
||||
public function getHeading(): string|HtmlString
|
||||
{
|
||||
$customer = $this->record;
|
||||
$total = $customer->donations()
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->sum('amount') / 100;
|
||||
|
||||
$giftAid = $customer->donations()
|
||||
->whereHas('donationPreferences', fn ($q) => $q->where('is_gift_aid', true))
|
||||
->exists();
|
||||
|
||||
$badges = '';
|
||||
if ($total >= 1000) {
|
||||
$badges .= ' <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 ml-2">⭐ Major Donor</span>';
|
||||
}
|
||||
if ($giftAid) {
|
||||
$badges .= ' <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 ml-2">Gift Aid</span>';
|
||||
}
|
||||
|
||||
$sg = $customer->scheduledGivingDonations()->where('is_active', true)->count();
|
||||
if ($sg > 0) {
|
||||
$badges .= ' <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">Monthly Supporter</span>';
|
||||
}
|
||||
|
||||
return new HtmlString($customer->name . $badges);
|
||||
}
|
||||
|
||||
public function getSubheading(): ?string
|
||||
{
|
||||
$customer = $this->record;
|
||||
$total = $customer->donations()
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->sum('amount') / 100;
|
||||
$count = $customer->donations()
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->count();
|
||||
$first = $customer->donations()->oldest()->first();
|
||||
$since = $first ? $first->created_at->format('M Y') : 'N/A';
|
||||
|
||||
return "£" . number_format($total, 2) . " donated across {$count} donations · Supporter since {$since}";
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$customer = $this->record;
|
||||
|
||||
return [
|
||||
Action::make('add_note')
|
||||
->label('Add Note')
|
||||
->icon('heroicon-o-chat-bubble-left-ellipsis')
|
||||
->color('gray')
|
||||
->form([
|
||||
Textarea::make('body')
|
||||
->label('Note')
|
||||
->placeholder("e.g. Called on " . now()->format('d M') . " — wants to update their address")
|
||||
->required()
|
||||
->rows(3),
|
||||
])
|
||||
->action(function (array $data) use ($customer) {
|
||||
$customer->internalNotes()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'body' => $data['body'],
|
||||
]);
|
||||
Notification::make()->title('Note added')->success()->send();
|
||||
}),
|
||||
|
||||
Action::make('resend_last_receipt')
|
||||
->label('Resend Last Receipt')
|
||||
->icon('heroicon-o-envelope')
|
||||
->color('info')
|
||||
->requiresConfirmation()
|
||||
->modalDescription('This will email the most recent donation receipt to ' . $customer->email)
|
||||
->visible(fn () => $customer->donations()->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))->exists())
|
||||
->action(function () use ($customer) {
|
||||
$donation = $customer->donations()
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($donation) {
|
||||
try {
|
||||
\Illuminate\Support\Facades\Mail::to($customer->email)
|
||||
->send(new \App\Mail\DonationConfirmed($donation));
|
||||
Notification::make()->title('Receipt sent to ' . $customer->email)->success()->send();
|
||||
} catch (\Throwable $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')
|
||||
->color('gray')
|
||||
->url(function () use ($customer) {
|
||||
$donation = $customer->donations()->whereNotNull('provider_reference')->latest()->first();
|
||||
if ($donation && $donation->provider_reference) {
|
||||
return 'https://dashboard.stripe.com/search?query=' . urlencode($customer->email);
|
||||
}
|
||||
return 'https://dashboard.stripe.com/search?query=' . urlencode($customer->email);
|
||||
})
|
||||
->openUrlInNewTab(),
|
||||
];
|
||||
}
|
||||
}
|
||||
21
temp_files/ListAppeals.php
Normal file
21
temp_files/ListAppeals.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\AppealResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AppealResource;
|
||||
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
|
||||
{
|
||||
return 'Every fundraising page on the site — both public supporter pages and charity campaigns. Search by name to find one.';
|
||||
}
|
||||
}
|
||||
21
temp_files/ListApprovalQueues.php
Normal file
21
temp_files/ListApprovalQueues.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ApprovalQueueResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ApprovalQueueResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListApprovalQueues extends ListRecords
|
||||
{
|
||||
protected static string $resource = ApprovalQueueResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Fundraiser Review Queue';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
return 'New fundraising pages that need your approval before going live. Use "AI Review All" to process in bulk, then approve or reject.';
|
||||
}
|
||||
}
|
||||
26
temp_files/ListCustomers.php
Normal file
26
temp_files/ListCustomers.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CustomerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CustomerResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCustomers extends ListRecords
|
||||
{
|
||||
protected static string $resource = CustomerResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Donor Lookup';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
return 'Search by name, email, or phone to find a donor. Click a donor to see their full history, send receipts, or add notes.';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
26
temp_files/ListDonations.php
Normal file
26
temp_files/ListDonations.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DonationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DonationResource;
|
||||
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
|
||||
{
|
||||
return 'Every one-off and monthly donation received. Use filters to narrow by date, cause, or status.';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
62
temp_files/OperationalHealthWidget.php
Normal file
62
temp_files/OperationalHealthWidget.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
86
temp_files/RecentDonationsWidget.php
Normal file
86
temp_files/RecentDonationsWidget.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Filament\Resources\CustomerResource;
|
||||
use App\Models\Donation;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
|
||||
class RecentDonationsWidget extends BaseWidget
|
||||
{
|
||||
protected static ?int $sort = 4;
|
||||
|
||||
protected int | string | array $columnSpan = 'full';
|
||||
|
||||
protected static ?string $heading = 'Latest Donations';
|
||||
|
||||
protected int $defaultPaginationPageOption = 5;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
Donation::query()
|
||||
->with(['customer', 'donationType', 'donationConfirmation', 'appeal'])
|
||||
->latest('created_at')
|
||||
)
|
||||
->columns([
|
||||
IconColumn::make('is_confirmed')
|
||||
->label('')
|
||||
->boolean()
|
||||
->getStateUsing(fn (Donation $r) => $r->isConfirmed())
|
||||
->trueIcon('heroicon-o-check-circle')
|
||||
->falseIcon('heroicon-o-x-circle')
|
||||
->trueColor('success')
|
||||
->falseColor('danger'),
|
||||
|
||||
TextColumn::make('customer.name')
|
||||
->label('Donor')
|
||||
->description(fn (Donation $d) => $d->customer?->email)
|
||||
->searchable(query: function (\Illuminate\Database\Eloquent\Builder $query, string $search) {
|
||||
$query->whereHas('customer', fn ($q) => $q
|
||||
->where('first_name', 'like', "%{$search}%")
|
||||
->orWhere('last_name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
);
|
||||
}),
|
||||
|
||||
TextColumn::make('amount')
|
||||
->label('Amount')
|
||||
->money('gbp', divideBy: 100)
|
||||
->sortable()
|
||||
->weight('bold'),
|
||||
|
||||
TextColumn::make('donationType.display_name')
|
||||
->label('Cause')
|
||||
->badge()
|
||||
->color('success')
|
||||
->limit(20),
|
||||
|
||||
TextColumn::make('appeal.name')
|
||||
->label('Fundraiser')
|
||||
->limit(20)
|
||||
->placeholder('Direct donation'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('When')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
Action::make('view_donor')
|
||||
->label('View Donor')
|
||||
->icon('heroicon-o-user')
|
||||
->url(fn (Donation $d) => $d->customer_id
|
||||
? CustomerResource::getUrl('edit', ['record' => $d->customer_id])
|
||||
: null)
|
||||
->visible(fn (Donation $d) => (bool) $d->customer_id),
|
||||
])
|
||||
->paginated([5, 10])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user