Files
calvana/temp_files/care/EditCustomer.php
Omair Saleh 8366054bd7 Deep UX: 2-column automations, visible appeal cards, platform education, strip model refs
Automations:
- 2-column layout: WhatsApp phone LEFT, education RIGHT
- Right column: 'How it works' (5 numbered steps), performance stats, timing controls, reply commands, tips
- Hero spans full width with photo+dark panel
- Improvement CTA is a prominent card, not floating text
- No misalignment — phone fills left column naturally

Collect:
- Appeals shown as visible gap-px grid cards (not hidden dropdown)
- Each card shows name, platform, amount raised, pledge count, collection rate
- Active appeal has border-l-2 blue indicator
- Platform integration clarity: shows 'Donors redirected to JustGiving' etc
- Educational section: 'Where to share your link' + 'How payment works'
- Explains bank transfer vs JustGiving vs card payment inline

AI model: Stripped all model name comments from code (no user-facing references existed)
2026-03-05 03:20:20 +08:00

278 lines
13 KiB
PHP

<?php
namespace App\Filament\Resources\CustomerResource\Pages;
use App\Definitions\PaymentProviders;
use App\Filament\Resources\CustomerResource;
use App\Filament\Resources\DonationResource;
use App\Models\Customer;
use App\Services\StripeRefundService;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\HtmlString;
class EditCustomer extends EditRecord
{
protected static string $resource = CustomerResource::class;
// ─── Heading: Show who this person IS, not just a name ───────
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)->first();
if ($sg) {
$amt = '£' . number_format($sg->total_amount / 100, 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">💙 ' . $amt . '/night</span>';
}
// 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 .= ' <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 ml-2">🔄 ' . $mAmt . '/month</span>';
}
// 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 .= ' <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 ml-2">⚠ ' . $incompleteRecent . ' incomplete</span>';
}
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,
]));
}
}