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)
This commit is contained in:
313
temp_files/care/EditScheduledGivingDonation.php
Normal file
313
temp_files/care/EditScheduledGivingDonation.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ScheduledGivingDonationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AppealResource\Pages\EditAppeal;
|
||||
use App\Filament\Resources\CustomerResource\Pages\EditCustomer;
|
||||
use App\Filament\Resources\ScheduledGivingDonationResource;
|
||||
use App\Models\ScheduledGivingDonation;
|
||||
use App\Services\StripeRefundService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* Scheduled/Regular Giving detail page — supporter care.
|
||||
*
|
||||
* Shows payment progress, card info, and provides cancel/refund
|
||||
* actions that sync with Stripe.
|
||||
*
|
||||
* @property ScheduledGivingDonation $record
|
||||
*/
|
||||
class EditScheduledGivingDonation extends EditRecord
|
||||
{
|
||||
protected static string $resource = ScheduledGivingDonationResource::class;
|
||||
|
||||
// ── Heading: status + progress at a glance ──────────────────
|
||||
|
||||
public function getHeading(): string|HtmlString
|
||||
{
|
||||
$d = $this->record;
|
||||
|
||||
$status = $d->is_active
|
||||
? '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">● Active</span>'
|
||||
: '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">● Cancelled</span>';
|
||||
|
||||
$amount = '£' . number_format($d->total_amount / 100, 2);
|
||||
|
||||
// Payment progress
|
||||
$totalPayments = $d->payments()->count();
|
||||
$paidPayments = $d->payments()->where('is_paid', true)->count();
|
||||
$paidTotal = $d->payments()->where('is_paid', true)->sum('amount') / 100;
|
||||
|
||||
$progress = $totalPayments > 0
|
||||
? '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-1">'
|
||||
. $paidPayments . '/' . $totalPayments . ' payments · £' . number_format($paidTotal, 2) . ' collected</span>'
|
||||
: '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 ml-1">No payments generated</span>';
|
||||
|
||||
if ($d->is_zakat) {
|
||||
$progress .= ' <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-700 ml-1">Zakat</span>';
|
||||
}
|
||||
if ($d->is_gift_aid) {
|
||||
$progress .= ' <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-700 ml-1">Gift Aid</span>';
|
||||
}
|
||||
|
||||
return new HtmlString(
|
||||
$amount . '/night — ' . e($d->customer?->name ?? 'Unknown')
|
||||
. '<div class="mt-1">' . $status . ' ' . $progress . '</div>'
|
||||
);
|
||||
}
|
||||
|
||||
public function getSubheading(): ?string
|
||||
{
|
||||
$d = $this->record;
|
||||
$parts = [];
|
||||
$parts[] = $d->scheduledGivingCampaign?->title ?? 'Unknown campaign';
|
||||
$parts[] = 'Started ' . $d->created_at?->format('d M Y');
|
||||
if ($d->customer?->email) {
|
||||
$parts[] = $d->customer->email;
|
||||
}
|
||||
if ($d->reference_code) {
|
||||
$parts[] = 'Ref: ' . $d->reference_code;
|
||||
}
|
||||
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
// ── Header Actions ──────────────────────────────────────────
|
||||
//
|
||||
// Priority order:
|
||||
// 1. Cancel (deactivate + detach card)
|
||||
// 2. Cancel & Refund All
|
||||
// 3. Activate
|
||||
// 4. Stripe Status
|
||||
// 5. Open in Stripe / View Donor / View Appeal / Email
|
||||
//
|
||||
|
||||
public function getHeaderActions(): array
|
||||
{
|
||||
$donation = $this->record;
|
||||
$hasStripe = (bool) $donation->stripe_setup_intent_id;
|
||||
|
||||
$paidCount = $donation->payments()
|
||||
->where('is_paid', true)
|
||||
->whereNotNull('stripe_payment_intent_id')
|
||||
->where('stripe_payment_intent_id', '!=', 'SKIPPED')
|
||||
->count();
|
||||
|
||||
$paidTotal = $donation->payments()
|
||||
->where('is_paid', true)
|
||||
->whereNotNull('stripe_payment_intent_id')
|
||||
->where('stripe_payment_intent_id', '!=', 'SKIPPED')
|
||||
->sum('amount') / 100;
|
||||
|
||||
return array_values(array_filter([
|
||||
|
||||
// ── CANCEL ──────────────────────────────────────────
|
||||
$donation->is_active ? Action::make('cancel')
|
||||
->label('Cancel')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalIcon('heroicon-o-x-circle')
|
||||
->modalHeading('Cancel Regular Giving')
|
||||
->modalDescription(
|
||||
'This will deactivate ' . e($donation->customer?->name ?? 'this donor') . '\'s regular giving.'
|
||||
. ($hasStripe ? "\n\nThe payment method will be detached from Stripe, preventing any future charges." : '')
|
||||
. "\n\nPreviously collected payments will NOT be refunded."
|
||||
)
|
||||
->modalSubmitActionLabel('Cancel Regular Giving')
|
||||
->action(function () {
|
||||
$donation = $this->record;
|
||||
$hasStripe = (bool) $donation->stripe_setup_intent_id;
|
||||
|
||||
$donation->update(['is_active' => false]);
|
||||
$stripeMsg = '';
|
||||
|
||||
if ($hasStripe) {
|
||||
$service = app(StripeRefundService::class);
|
||||
$result = $service->detachPaymentMethod($donation->stripe_setup_intent_id);
|
||||
$stripeMsg = $result['success']
|
||||
? ($result['message'] ?? 'Payment method detached.')
|
||||
: 'Warning: ' . ($result['error'] ?? 'Could not detach payment method.');
|
||||
}
|
||||
|
||||
$donation->internalNotes()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'body' => 'Regular giving cancelled by admin. ' . $stripeMsg,
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('Regular giving cancelled')
|
||||
->body($stripeMsg ?: 'Deactivated')
|
||||
->success()
|
||||
->send();
|
||||
}) : null,
|
||||
|
||||
// ── CANCEL & REFUND ALL ─────────────────────────────
|
||||
$donation->is_active && $paidCount > 0 ? Action::make('cancel_and_refund')
|
||||
->label('Cancel & Refund All')
|
||||
->icon('heroicon-o-arrow-uturn-left')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalIcon('heroicon-o-exclamation-triangle')
|
||||
->modalHeading('Cancel & Refund All Payments')
|
||||
->modalDescription(
|
||||
"This will:\n"
|
||||
. "1. Deactivate the regular giving\n"
|
||||
. "2. Detach the payment method from Stripe\n"
|
||||
. "3. Refund all {$paidCount} collected payments (£" . number_format($paidTotal, 2) . ")\n\n"
|
||||
. "This cannot be undone."
|
||||
)
|
||||
->modalSubmitActionLabel('Cancel & Refund Everything')
|
||||
->action(function () {
|
||||
$donation = $this->record;
|
||||
$donation->update(['is_active' => false]);
|
||||
|
||||
$service = app(StripeRefundService::class);
|
||||
|
||||
// Detach card
|
||||
if ($donation->stripe_setup_intent_id) {
|
||||
$service->detachPaymentMethod($donation->stripe_setup_intent_id);
|
||||
}
|
||||
|
||||
// Refund all paid payments
|
||||
$paidPayments = $donation->payments()
|
||||
->where('is_paid', true)
|
||||
->whereNotNull('stripe_payment_intent_id')
|
||||
->where('stripe_payment_intent_id', '!=', 'SKIPPED')
|
||||
->get();
|
||||
|
||||
$refunded = 0;
|
||||
$failed = 0;
|
||||
$totalRefunded = 0;
|
||||
|
||||
foreach ($paidPayments as $payment) {
|
||||
$result = $service->refundPaymentIntent(
|
||||
$payment->stripe_payment_intent_id,
|
||||
null,
|
||||
'Bulk cancel & refund — payment #' . $payment->id
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
$payment->update(['is_paid' => false]);
|
||||
$refunded++;
|
||||
$totalRefunded += $result['amount'];
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
$donation->internalNotes()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'body' => 'Cancelled & refunded all payments. '
|
||||
. $refunded . ' refunded (£' . number_format($totalRefunded / 100, 2) . ')'
|
||||
. ($failed > 0 ? ", {$failed} failed" : ''),
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('Cancelled & Refunded')
|
||||
->body(
|
||||
$refunded . ' payments refunded (£' . number_format($totalRefunded / 100, 2) . ')'
|
||||
. ($failed > 0 ? ". {$failed} failed — check Stripe." : '')
|
||||
)
|
||||
->success()
|
||||
->send();
|
||||
}) : null,
|
||||
|
||||
// ── REACTIVATE ──────────────────────────────────────
|
||||
! $donation->is_active ? Action::make('activate')
|
||||
->label('Reactivate')
|
||||
->icon('heroicon-o-play')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Reactivate Regular Giving')
|
||||
->modalDescription(
|
||||
'Re-enable payment collection. Note: if the payment method '
|
||||
. 'was previously detached, future charges may fail.'
|
||||
)
|
||||
->action(function () {
|
||||
$donation = $this->record;
|
||||
$donation->update(['is_active' => true]);
|
||||
|
||||
$donation->internalNotes()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'body' => 'Regular giving reactivated by admin.',
|
||||
]);
|
||||
|
||||
Notification::make()->title('Regular giving reactivated')->success()->send();
|
||||
}) : null,
|
||||
|
||||
// ── STRIPE STATUS ───────────────────────────────────
|
||||
$hasStripe ? Action::make('stripe_status')
|
||||
->label('Stripe Status')
|
||||
->icon('heroicon-o-magnifying-glass')
|
||||
->color('gray')
|
||||
->action(function () {
|
||||
$donation = $this->record;
|
||||
$service = app(StripeRefundService::class);
|
||||
$details = $service->getSetupIntentDetails($donation->stripe_setup_intent_id);
|
||||
|
||||
if (! $details) {
|
||||
Notification::make()
|
||||
->title('Could not retrieve Stripe details')
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$body = 'Status: ' . $details['status'];
|
||||
if ($details['card_brand'] && $details['card_last4']) {
|
||||
$body .= "\nCard: " . ucfirst($details['card_brand']) . ' ····' . $details['card_last4'];
|
||||
}
|
||||
if ($details['card_exp_month'] && $details['card_exp_year']) {
|
||||
$body .= ' (expires ' . $details['card_exp_month'] . '/' . $details['card_exp_year'] . ')';
|
||||
}
|
||||
$body .= "\nCreated: " . $details['created'];
|
||||
if ($details['customer_id']) {
|
||||
$body .= "\nStripe Customer: " . $details['customer_id'];
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Stripe SetupIntent')
|
||||
->body($body)
|
||||
->info()
|
||||
->persistent()
|
||||
->send();
|
||||
}) : null,
|
||||
|
||||
// ── OPEN IN STRIPE ──────────────────────────────────
|
||||
$hasStripe ? Action::make('open_stripe')
|
||||
->label('Stripe ↗')
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->color('gray')
|
||||
->url('https://dashboard.stripe.com/search?query=' . urlencode($donation->stripe_setup_intent_id))
|
||||
->openUrlInNewTab() : null,
|
||||
|
||||
// ── VIEW DONOR ──────────────────────────────────────
|
||||
$donation->customer ? Action::make('view_customer')
|
||||
->label('Donor')
|
||||
->icon('heroicon-o-user')
|
||||
->color('gray')
|
||||
->url(EditCustomer::getUrl(['record' => $donation->customer->id]))
|
||||
: null,
|
||||
|
||||
// ── VIEW APPEAL ─────────────────────────────────────
|
||||
$donation->appeal ? Action::make('view_appeal')
|
||||
->label('Fundraiser')
|
||||
->icon('heroicon-o-heart')
|
||||
->color('gray')
|
||||
->url(EditAppeal::getUrl(['record' => $donation->appeal->id]))
|
||||
: null,
|
||||
|
||||
// ── EMAIL ───────────────────────────────────────────
|
||||
$donation->customer?->email ? Action::make('email')
|
||||
->label('Email')
|
||||
->icon('heroicon-o-at-symbol')
|
||||
->color('gray')
|
||||
->url('mailto:' . $donation->customer->email)
|
||||
->openUrlInNewTab() : null,
|
||||
]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user