Files
calvana/temp_files/fix2/ListScheduledGivingDonations.php
Omair Saleh 50d449e2b7 feat: conditional & match funding pledges — deeply integrated across entire product
- Schema: isConditional, conditionType, conditionText, conditionThreshold, conditionMet, conditionMetAt on Pledge
- Pledge form: 'This is a match pledge' toggle after amount selection
  - Two modes: threshold (if target is reached) and match (match funding)
  - Goal amount passed through from event
- Auto-trigger: when total raised hits threshold, conditional pledges unlock automatically
  - WhatsApp notification sent to donor when unlocked
  - Threshold check runs after every pledge creation AND every status change
- Cron: skips conditional pledges until conditionMet=true (no premature reminders)
- Dashboard Home: progress bar shows conditional segment (amber), stats grid adds Conditional column
- Dashboard Money: conditional/unlocked badge on pledge rows
- Dashboard Collect: hero shows conditional total in amber
- Dashboard Reports: financial summary shows conditional breakdown
- Donor 'My Pledges': conditional card with condition text + activation status
- Confirmation step: specialized messaging for match pledges
- CRM export: includes is_conditional, condition_type, condition_text, condition_met columns
- Status guide: conditional status explained in human language
2026-03-05 04:19:23 +08:00

167 lines
6.2 KiB
PHP

<?php
namespace App\Filament\Resources\ScheduledGivingDonationResource\Pages;
use App\Filament\Resources\ScheduledGivingDonationResource;
use App\Models\ScheduledGivingCampaign;
use App\Models\ScheduledGivingDonation;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Database\Eloquent\Builder;
class ListScheduledGivingDonations extends ListRecords
{
protected static string $resource = ScheduledGivingDonationResource::class;
public function getHeading(): string
{
return 'Scheduled Giving';
}
public function getSubheading(): string
{
$current = $this->currentSeasonCount();
return "{$current} subscribers this season.";
}
/** Count real current-season subscribers using the model directly (safe) */
private function currentSeasonCount(): int
{
return ScheduledGivingDonation::query()
->whereNotNull('customer_id')
->where('total_amount', '>', 0)
->whereNull('deleted_at')
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
->count();
}
/**
* Apply "real subscriber" filter using whereIn subqueries
* instead of whereHas — avoids null model crash during tab init.
*/
private function applyReal(Builder $q): Builder
{
return $q
->whereNotNull('customer_id')
->where('total_amount', '>', 0)
->whereNull('scheduled_giving_donations.deleted_at')
->whereIn('scheduled_giving_donations.id', fn ($sub) => $sub
->select('scheduled_giving_donation_id')
->from('scheduled_giving_payments')
->whereNull('deleted_at'));
}
/** Real + has future payment = current season */
private function applyCurrentSeason(Builder $q): Builder
{
return $this->applyReal($q)
->whereIn('scheduled_giving_donations.id', fn ($sub) => $sub
->select('scheduled_giving_donation_id')
->from('scheduled_giving_payments')
->whereNull('deleted_at')
->where('expected_at', '>', now()));
}
/** Real + NO future payments = expired */
private function applyExpired(Builder $q): Builder
{
return $this->applyReal($q)
->whereNotIn('scheduled_giving_donations.id', fn ($sub) => $sub
->select('scheduled_giving_donation_id')
->from('scheduled_giving_payments')
->whereNull('deleted_at')
->where('expected_at', '>', now()));
}
public function getTabs(): array
{
$campaigns = ScheduledGivingCampaign::all();
$currentCount = $this->currentSeasonCount();
$tabs = [];
$tabs['current'] = Tab::make('This Season')
->icon('heroicon-o-sun')
->badge($currentCount)
->badgeColor('success')
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q));
foreach ($campaigns as $c) {
$slug = str($c->title)->slug()->toString();
$count = ScheduledGivingDonation::query()
->whereNotNull('customer_id')
->where('total_amount', '>', 0)
->whereNull('deleted_at')
->where('scheduled_giving_campaign_id', $c->id)
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
->count();
if ($count === 0) continue;
$tabs[$slug] = Tab::make($c->title)
->icon('heroicon-o-calendar')
->badge($count)
->badgeColor('primary')
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q)
->where('scheduled_giving_campaign_id', $c->id));
}
// Failed (current season)
$failedCount = ScheduledGivingDonation::query()
->whereNotNull('customer_id')
->where('total_amount', '>', 0)
->whereNull('deleted_at')
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
->whereHas('payments', fn ($q) => $q->where('is_paid', false)->where('attempts', '>', 0)->whereNull('deleted_at'))
->count();
if ($failedCount > 0) {
$tabs['failed'] = Tab::make('Failed')
->icon('heroicon-o-exclamation-triangle')
->badge($failedCount)
->badgeColor('danger')
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q)
->whereIn('scheduled_giving_donations.id', fn ($sub) => $sub
->select('scheduled_giving_donation_id')
->from('scheduled_giving_payments')
->where('is_paid', false)
->where('attempts', '>', 0)
->whereNull('deleted_at')));
}
// Past seasons
$expiredCount = ScheduledGivingDonation::query()
->whereNotNull('customer_id')
->where('total_amount', '>', 0)
->whereNull('deleted_at')
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
->whereDoesntHave('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
->count();
$tabs['past'] = Tab::make('Past Seasons')
->icon('heroicon-o-archive-box')
->badge($expiredCount > 0 ? $expiredCount : null)
->badgeColor('gray')
->modifyQueryUsing(fn (Builder $q) => $this->applyExpired($q));
$tabs['all'] = Tab::make('All')
->icon('heroicon-o-squares-2x2')
->modifyQueryUsing(fn (Builder $q) => $this->applyReal($q));
return $tabs;
}
protected function getHeaderActions(): array
{
return [];
}
public function getDefaultActiveTab(): string|int|null
{
return 'current';
}
}