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 .= ' ⭐ Major Donor'; } if ($giftAid) { $badges .= ' Gift Aid'; } $sg = $customer->scheduledGivingDonations()->where('is_active', true)->first(); if ($sg) { $amt = '£' . number_format($sg->total_amount / 100, 0); $badges .= ' 💙 ' . $amt . '/night'; } // Monthly donations (reoccurrence=2) $monthly = $customer->donations() ->where('reoccurrence', 2) ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->first(); if ($monthly) { $mAmt = '£' . number_format($monthly->amount / 100, 0); $badges .= ' 🔄 ' . $mAmt . '/month'; } // Recent incomplete donations $incompleteRecent = $customer->donations() ->whereDoesntHave('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->where('created_at', '>=', now()->subDays(7)) ->count(); if ($incompleteRecent > 0) { $badges .= ' ⚠ ' . $incompleteRecent . ' incomplete'; } return new HtmlString(e($customer->name) . $badges); } // ─── Subheading: The one-line story of this donor ──────────── public function getSubheading(): ?string { $customer = $this->record; $confirmed = $customer->donations() ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')); $total = $confirmed->sum('amount') / 100; $count = $confirmed->count(); $first = $customer->donations()->oldest()->first(); $since = $first ? $first->created_at->format('M Y') : null; $parts = []; if ($total > 0) { $parts[] = '£' . number_format($total, 2) . ' donated across ' . $count . ' donations'; } else { $parts[] = 'No confirmed donations yet'; } if ($since) { $parts[] = 'Supporter since ' . $since; } return implode(' · ', $parts); } // ─── Header Actions ───────────────────────────────────────── protected function getHeaderActions(): array { $customer = $this->record; // Pre-compute recurring counts for visibility checks $activeScheduled = $customer->scheduledGivingDonations() ->where('is_active', true)->count(); $activeMonthly = $customer->donations() ->where('reoccurrence', 2) ->where('provider_type', PaymentProviders::STRIPE) ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->count(); $hasRecurring = $activeScheduled > 0 || $activeMonthly > 0; return array_values(array_filter([ // ── CANCEL ALL RECURRING ──────────────────────────── $hasRecurring ? Action::make('cancel_all_recurring') ->label('Cancel All Recurring') ->icon('heroicon-o-x-circle') ->color('danger') ->requiresConfirmation() ->modalIcon('heroicon-o-exclamation-triangle') ->modalHeading('Cancel All Recurring Giving') ->modalDescription(function () use ($customer, $activeScheduled, $activeMonthly) { $parts = []; if ($activeScheduled > 0) { $sgTotal = $customer->scheduledGivingDonations() ->where('is_active', true)->sum('total_amount') / 100; $parts[] = $activeScheduled . ' regular giving ' . ($activeScheduled === 1 ? 'subscription' : 'subscriptions') . ' (£' . number_format($sgTotal, 2) . ' total)'; } if ($activeMonthly > 0) { $parts[] = $activeMonthly . ' monthly ' . ($activeMonthly === 1 ? 'donation' : 'donations'); } return "This will cancel:\n" . implode("\n", array_map(fn ($p) => "• {$p}", $parts)) . "\n\nAll Stripe payment methods will be detached. " . "Previously collected payments will NOT be refunded."; }) ->modalSubmitActionLabel('Cancel All Recurring') ->action(function () use ($customer) { $service = app(StripeRefundService::class); $cancelled = 0; // Cancel active scheduled giving $activeGiving = $customer->scheduledGivingDonations() ->where('is_active', true)->get(); foreach ($activeGiving as $sg) { $sg->update(['is_active' => false]); if ($sg->stripe_setup_intent_id) { $service->detachPaymentMethod($sg->stripe_setup_intent_id); } if (method_exists($sg, 'internalNotes')) { $sg->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => 'Cancelled via donor profile "Cancel All Recurring".', ]); } $cancelled++; } // Cancel monthly Stripe donations $monthlyDonations = $customer->donations() ->where('reoccurrence', 2) ->where('provider_type', PaymentProviders::STRIPE) ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->get(); foreach ($monthlyDonations as $d) { $ref = $d->provider_reference ?? ''; if (str_starts_with($ref, 'seti_')) { $service->detachPaymentMethod($ref); } $d->donationConfirmation?->update(['confirmed_at' => null]); $d->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => 'Monthly giving cancelled via donor profile "Cancel All Recurring".', ]); $cancelled++; } // Log on the customer too $customer->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => "All recurring giving cancelled ({$cancelled} items). Payment methods detached from Stripe.", ]); Notification::make() ->title("Cancelled {$cancelled} recurring " . ($cancelled === 1 ? 'item' : 'items')) ->success() ->send(); }) : null, // ── ADD NOTE ──────────────────────────────────────── 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(); }), // ── RESEND RECEIPT ────────────────────────────────── $customer->donations() ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->exists() ? Action::make('resend_receipt') ->label('Resend Receipt') ->icon('heroicon-o-envelope') ->color('info') ->form([ Select::make('donation_id') ->label('Which donation?') ->options(function () use ($customer) { return $customer->donations() ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->latest() ->take(10) ->get() ->mapWithKeys(function ($d) { $label = '£' . number_format($d->amount / 100, 2) . ' on ' . $d->created_at->format('d M Y') . ' — ' . ($d->donationType?->display_name ?? 'Unknown'); return [$d->id => $label]; }); }) ->required() ->helperText('Select the donation to resend the receipt for'), ]) ->action(function (array $data) use ($customer) { $donation = $customer->donations()->find($data['donation_id']); if ($donation) { try { Mail::to($customer->email) ->send(new \App\Mail\DonationConfirmed($donation)); Notification::make() ->title('Receipt sent to ' . $customer->email) ->body('For £' . number_format($donation->amount / 100, 2) . ' on ' . $donation->created_at->format('d M Y')) ->success() ->send(); } catch (\Throwable $e) { Notification::make()->title('Failed to send receipt')->body($e->getMessage())->danger()->send(); } } }) : null, // ── VIEW IN STRIPE ────────────────────────────────── Action::make('view_in_stripe') ->label('Stripe') ->icon('heroicon-o-arrow-top-right-on-square') ->color('gray') ->url('https://dashboard.stripe.com/search?query=' . urlencode($customer->email)) ->openUrlInNewTab(), // ── EMAIL ─────────────────────────────────────────── $customer->email ? Action::make('email_donor') ->label('Email') ->icon('heroicon-o-at-symbol') ->color('gray') ->url('mailto:' . $customer->email) ->openUrlInNewTab() : null, ])); } }