0 + not soft-deleted). */ class ScheduledGivingDashboard extends Page { protected static ?string $navigationIcon = 'heroicon-o-calendar-days'; protected static ?string $navigationGroup = 'Giving'; protected static ?int $navigationSort = 0; protected static ?string $navigationLabel = 'Dashboard'; protected static ?string $title = 'Scheduled Giving'; protected static string $view = 'filament.pages.scheduled-giving-dashboard'; /** Real subscriber IDs (has customer + payments + amount > 0 + not soft-deleted) */ private function realSubscriberIds(?int $campaignId = null) { $q = DB::table('scheduled_giving_donations as d') ->whereNotNull('d.customer_id') ->where('d.total_amount', '>', 0) ->whereNull('d.deleted_at') ->whereExists(function ($sub) { $sub->select(DB::raw(1)) ->from('scheduled_giving_payments as p') ->whereColumn('p.scheduled_giving_donation_id', 'd.id') ->whereNull('p.deleted_at'); }); if ($campaignId) { $q->where('d.scheduled_giving_campaign_id', $campaignId); } return $q->pluck('d.id'); } /** IDs with at least one future payment = current season */ private function currentSeasonIds(?int $campaignId = null) { $realIds = $this->realSubscriberIds($campaignId); if ($realIds->isEmpty()) return collect(); return DB::table('scheduled_giving_donations as d') ->whereIn('d.id', $realIds) ->whereExists(function ($sub) { $sub->select(DB::raw(1)) ->from('scheduled_giving_payments as p') ->whereColumn('p.scheduled_giving_donation_id', 'd.id') ->whereNull('p.deleted_at') ->whereRaw('p.expected_at > NOW()'); }) ->pluck('d.id'); } public function getCampaignData(): array { $campaigns = ScheduledGivingCampaign::all(); $result = []; foreach ($campaigns as $c) { $realIds = $this->realSubscriberIds($c->id); if ($realIds->isEmpty()) { $result[] = $this->emptyCampaign($c); continue; } $currentIds = $this->currentSeasonIds($c->id); $expiredIds = $realIds->diff($currentIds); // Current season payment stats — separate due vs future $currentPayments = null; if ($currentIds->isNotEmpty()) { $currentPayments = DB::table('scheduled_giving_payments') ->whereIn('scheduled_giving_donation_id', $currentIds) ->whereNull('deleted_at') ->selectRaw(" COUNT(*) as total, SUM(is_paid = 1) as paid, SUM(is_paid = 0 AND expected_at <= NOW()) as failed, SUM(is_paid = 0 AND expected_at > NOW()) as scheduled, SUM(expected_at <= NOW()) as due, SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) as collected, SUM(CASE WHEN is_paid = 0 AND expected_at <= NOW() THEN amount ELSE 0 END) as failed_amount, SUM(CASE WHEN is_paid = 0 AND expected_at > NOW() THEN amount ELSE 0 END) as scheduled_amount, AVG(CASE WHEN is_paid = 1 THEN amount ELSE NULL END) as avg_amount, MIN(CASE WHEN is_paid = 0 AND expected_at > NOW() THEN expected_at ELSE NULL END) as next_payment ") ->first(); } // All-time payment stats (for totals) $allPayments = DB::table('scheduled_giving_payments') ->whereIn('scheduled_giving_donation_id', $realIds) ->whereNull('deleted_at') ->selectRaw(" SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) as collected, SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) as paid, COUNT(*) as total ") ->first(); // Completion for current season $totalNights = count($c->dates ?? []); $fullyPaid = 0; if ($totalNights > 0 && $currentIds->isNotEmpty()) { $ids = $currentIds->implode(','); $row = DB::selectOne("SELECT COUNT(*) as cnt FROM (SELECT scheduled_giving_donation_id FROM scheduled_giving_payments WHERE scheduled_giving_donation_id IN ({$ids}) AND deleted_at IS NULL GROUP BY scheduled_giving_donation_id HAVING SUM(is_paid) >= {$totalNights}) sub"); $fullyPaid = $row->cnt ?? 0; } $due = (int) ($currentPayments->due ?? 0); $paid = (int) ($currentPayments->paid ?? 0); $result[] = [ 'campaign' => $c, 'all_time_subscribers' => $realIds->count(), 'all_time_collected' => ($allPayments->collected ?? 0) / 100, 'all_time_payments' => (int) ($allPayments->total ?? 0), 'all_time_paid' => (int) ($allPayments->paid ?? 0), // Current season 'current_subscribers' => $currentIds->count(), 'expired_subscribers' => $expiredIds->count(), 'total_payments' => (int) ($currentPayments->total ?? 0), 'due_payments' => $due, 'paid_payments' => $paid, 'failed_payments' => (int) ($currentPayments->failed ?? 0), 'scheduled_payments' => (int) ($currentPayments->scheduled ?? 0), 'collected' => ($currentPayments->collected ?? 0) / 100, 'failed_amount' => ($currentPayments->failed_amount ?? 0) / 100, 'scheduled_amount' => ($currentPayments->scheduled_amount ?? 0) / 100, 'avg_per_night' => ($currentPayments->avg_amount ?? 0) / 100, 'collection_rate' => $due > 0 ? round($paid / $due * 100, 1) : 0, 'fully_completed' => $fullyPaid, 'dates' => $c->dates ?? [], 'total_nights' => $totalNights, 'next_payment' => $currentPayments->next_payment ?? null, ]; } return $result; } public function getGlobalStats(): array { $realIds = $this->realSubscriberIds(); $currentIds = $this->currentSeasonIds(); $allTime = DB::table('scheduled_giving_payments') ->whereIn('scheduled_giving_donation_id', $realIds) ->whereNull('deleted_at') ->selectRaw(" SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) / 100 as collected, SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) as paid, COUNT(*) as total ") ->first(); $currentStats = null; if ($currentIds->isNotEmpty()) { $currentStats = DB::table('scheduled_giving_payments') ->whereIn('scheduled_giving_donation_id', $currentIds) ->whereNull('deleted_at') ->selectRaw(" SUM(is_paid = 1) as paid, SUM(expected_at <= NOW()) as due, SUM(is_paid = 0 AND expected_at <= NOW()) as failed, SUM(is_paid = 0 AND expected_at > NOW()) as scheduled, SUM(CASE WHEN is_paid = 1 THEN amount ELSE 0 END) / 100 as collected, SUM(CASE WHEN is_paid = 0 AND expected_at <= NOW() THEN amount ELSE 0 END) / 100 as failed_amount, SUM(CASE WHEN is_paid = 0 AND expected_at > NOW() THEN amount ELSE 0 END) / 100 as scheduled_amount ") ->first(); } $due = (int) ($currentStats->due ?? 0); $paid = (int) ($currentStats->paid ?? 0); return [ 'total_subscribers' => $realIds->count(), 'current_subscribers' => $currentIds->count(), 'expired_subscribers' => $realIds->count() - $currentIds->count(), 'all_time_collected' => (float) ($allTime->collected ?? 0), 'collected' => (float) ($currentStats->collected ?? 0), 'failed_amount' => (float) ($currentStats->failed_amount ?? 0), 'scheduled_amount' => (float) ($currentStats->scheduled_amount ?? 0), 'failed_count' => (int) ($currentStats->failed ?? 0), 'scheduled_count' => (int) ($currentStats->scheduled ?? 0), 'due_payments' => $due, 'paid_payments' => $paid, 'collection_rate' => $due > 0 ? round($paid / $due * 100, 1) : 0, ]; } public function getFailedPayments(): array { $currentIds = $this->currentSeasonIds(); if ($currentIds->isEmpty()) return []; return DB::table('scheduled_giving_payments as p') ->join('scheduled_giving_donations as d', 'd.id', '=', 'p.scheduled_giving_donation_id') ->join('scheduled_giving_campaigns as c', 'c.id', '=', 'd.scheduled_giving_campaign_id') ->leftJoin('customers as cu', 'cu.id', '=', 'd.customer_id') ->whereIn('d.id', $currentIds) ->where('p.is_paid', false) ->where('p.attempts', '>', 0) ->whereNull('p.deleted_at') ->orderByDesc('p.updated_at') ->limit(15) ->get([ 'p.id as payment_id', 'p.amount', 'p.expected_at', 'p.attempts', 'd.id as donation_id', 'c.title as campaign', DB::raw("CONCAT(cu.first_name, ' ', cu.last_name) as donor_name"), 'cu.email as donor_email', ]) ->toArray(); } public function getUpcomingPayments(): array { $currentIds = $this->currentSeasonIds(); if ($currentIds->isEmpty()) return []; return DB::table('scheduled_giving_payments as p') ->join('scheduled_giving_donations as d', 'd.id', '=', 'p.scheduled_giving_donation_id') ->join('scheduled_giving_campaigns as c', 'c.id', '=', 'd.scheduled_giving_campaign_id') ->whereIn('d.id', $currentIds) ->where('p.is_paid', false) ->where('p.attempts', 0) ->where('d.is_active', true) ->whereNull('p.deleted_at') ->whereBetween('p.expected_at', [now(), now()->addHours(48)]) ->selectRaw(" c.title as campaign, COUNT(*) as payment_count, SUM(p.amount) / 100 as total_amount, MIN(p.expected_at) as earliest ") ->groupBy('c.title') ->orderBy('earliest') ->get() ->toArray(); } public function getDataQuality(): array { $total = DB::table('scheduled_giving_donations')->count(); $softDeleted = DB::table('scheduled_giving_donations')->whereNotNull('deleted_at')->count(); $noCustomer = DB::table('scheduled_giving_donations')->whereNull('customer_id')->whereNull('deleted_at')->count(); $noPayments = DB::table('scheduled_giving_donations as d') ->whereNull('d.deleted_at') ->whereNotNull('d.customer_id') ->whereNotExists(function ($q) { $q->select(DB::raw(1)) ->from('scheduled_giving_payments as p') ->whereColumn('p.scheduled_giving_donation_id', 'd.id') ->whereNull('p.deleted_at'); })->count(); $zeroAmount = DB::table('scheduled_giving_donations') ->whereNull('deleted_at') ->where('total_amount', '<=', 0) ->count(); return [ 'total_records' => $total, 'soft_deleted' => $softDeleted, 'no_customer' => $noCustomer, 'no_payments' => $noPayments, 'zero_amount' => $zeroAmount, ]; } private function emptyCampaign($c): array { return [ 'campaign' => $c, 'all_time_subscribers' => 0, 'all_time_collected' => 0, 'all_time_payments' => 0, 'all_time_paid' => 0, 'current_subscribers' => 0, 'expired_subscribers' => 0, 'current_payments' => 0, 'current_paid' => 0, 'current_pending' => 0, 'current_failed' => 0, 'current_collected' => 0, 'current_pending_amount' => 0, 'avg_per_night' => 0, 'fully_completed' => 0, 'dates' => $c->dates ?? [], 'total_nights' => count($c->dates ?? []), 'next_payment' => null, ]; } }