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
This commit is contained in:
110
temp_files/fix2/ListDonations.php
Normal file
110
temp_files/fix2/ListDonations.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DonationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DonationResource;
|
||||
use App\Models\Donation;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ListDonations extends ListRecords
|
||||
{
|
||||
protected static string $resource = DonationResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Donations';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$todayCount = Donation::whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereDate('created_at', today())
|
||||
->count();
|
||||
$todayAmount = Donation::whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereDate('created_at', today())
|
||||
->sum('amount') / 100;
|
||||
|
||||
return "Today: {$todayCount} confirmed (£" . number_format($todayAmount, 0) . ")";
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$incompleteCount = Donation::whereDoesntHave('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->where('created_at', '>=', now()->subDays(7))
|
||||
->count();
|
||||
|
||||
$recurring = Donation::where('reoccurrence', '!=', -1)
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->count();
|
||||
|
||||
// Use whereIn with subquery instead of whereHas to avoid null model crash
|
||||
// during Filament tab initialization (modifyQueryUsing gets a builder with no model)
|
||||
$confirmedSubquery = fn (Builder $q) => $q->whereIn(
|
||||
'donations.id',
|
||||
fn ($sub) => $sub->select('donation_id')
|
||||
->from('donation_confirmations')
|
||||
->whereNotNull('confirmed_at')
|
||||
);
|
||||
|
||||
$unconfirmedSubquery = fn (Builder $q) => $q->whereNotIn(
|
||||
'donations.id',
|
||||
fn ($sub) => $sub->select('donation_id')
|
||||
->from('donation_confirmations')
|
||||
->whereNotNull('confirmed_at')
|
||||
);
|
||||
|
||||
return [
|
||||
'today' => Tab::make('Today')
|
||||
->icon('heroicon-o-clock')
|
||||
->modifyQueryUsing(fn (Builder $q) => $confirmedSubquery($q)
|
||||
->whereDate('created_at', today())
|
||||
),
|
||||
|
||||
'all_confirmed' => Tab::make('All Confirmed')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->modifyQueryUsing(fn (Builder $q) => $confirmedSubquery($q)),
|
||||
|
||||
'incomplete' => Tab::make('Incomplete')
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->badge($incompleteCount > 0 ? $incompleteCount : null)
|
||||
->badgeColor('danger')
|
||||
->modifyQueryUsing(fn (Builder $q) => $unconfirmedSubquery($q)
|
||||
->where('created_at', '>=', now()->subDays(7))
|
||||
),
|
||||
|
||||
'zakat' => Tab::make('Zakat')
|
||||
->icon('heroicon-o-star')
|
||||
->modifyQueryUsing(fn (Builder $q) => $confirmedSubquery($q)
|
||||
->whereIn('donations.id', fn ($sub) => $sub->select('donation_id')
|
||||
->from('donation_preferences')
|
||||
->where('is_zakat', true))
|
||||
),
|
||||
|
||||
'gift_aid' => Tab::make('Gift Aid')
|
||||
->icon('heroicon-o-gift')
|
||||
->modifyQueryUsing(fn (Builder $q) => $confirmedSubquery($q)
|
||||
->whereIn('donations.id', fn ($sub) => $sub->select('donation_id')
|
||||
->from('donation_preferences')
|
||||
->where('is_gift_aid', true))
|
||||
),
|
||||
|
||||
'recurring' => Tab::make('Recurring')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->badge($recurring > 0 ? $recurring : null)
|
||||
->badgeColor('info')
|
||||
->modifyQueryUsing(fn (Builder $q) => $confirmedSubquery($q)
|
||||
->where('reoccurrence', '!=', -1)
|
||||
),
|
||||
|
||||
'everything' => Tab::make('Everything')
|
||||
->icon('heroicon-o-squares-2x2'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultActiveTab(): string | int | null
|
||||
{
|
||||
return 'today';
|
||||
}
|
||||
}
|
||||
@@ -20,88 +20,85 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$current = $this->currentSeasonScope()->count();
|
||||
$current = $this->currentSeasonCount();
|
||||
return "{$current} subscribers this season.";
|
||||
}
|
||||
|
||||
/** Real subscriber: has customer, has payments, amount > 0, not soft-deleted */
|
||||
private function realScope(): Builder
|
||||
/** 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('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'));
|
||||
->whereNull('deleted_at')
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
|
||||
->count();
|
||||
}
|
||||
|
||||
/** Current season: real + has at least one future payment */
|
||||
private function currentSeasonScope(): Builder
|
||||
{
|
||||
return $this->realScope()
|
||||
->whereHas('payments', fn ($q) => $q
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Applies real + current season filters to the query */
|
||||
private function applyCurrentSeason(Builder $q): Builder
|
||||
{
|
||||
return $q
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($sub) => $sub->whereNull('deleted_at'))
|
||||
->whereHas('payments', fn ($sub) => $sub
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Applies real + expired (no future payments) filters */
|
||||
private function applyExpired(Builder $q): Builder
|
||||
{
|
||||
return $q
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($sub) => $sub->whereNull('deleted_at'))
|
||||
->whereDoesntHave('payments', fn ($sub) => $sub
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Applies real subscriber filters */
|
||||
/**
|
||||
* 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')
|
||||
->whereHas('payments', fn ($sub) => $sub->whereNull('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->currentSeasonScope()->count();
|
||||
$currentCount = $this->currentSeasonCount();
|
||||
|
||||
$tabs = [];
|
||||
|
||||
// Current season — the primary tab
|
||||
$tabs['current'] = Tab::make('This Season')
|
||||
->icon('heroicon-o-sun')
|
||||
->badge($currentCount)
|
||||
->badgeColor('success')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q));
|
||||
|
||||
// Per-campaign tabs for current season
|
||||
foreach ($campaigns as $c) {
|
||||
$slug = str($c->title)->slug()->toString();
|
||||
$count = $this->currentSeasonScope()
|
||||
$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; // Skip campaigns with no current subscribers
|
||||
if ($count === 0) continue;
|
||||
|
||||
$tabs[$slug] = Tab::make($c->title)
|
||||
->icon('heroicon-o-calendar')
|
||||
@@ -111,12 +108,14 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
->where('scheduled_giving_campaign_id', $c->id));
|
||||
}
|
||||
|
||||
// Failed (current season only)
|
||||
$failedCount = $this->currentSeasonScope()
|
||||
->whereHas('payments', fn ($q) => $q
|
||||
->where('is_paid', false)
|
||||
->where('attempts', '>', 0)
|
||||
->whereNull('deleted_at'))
|
||||
// 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) {
|
||||
@@ -125,17 +124,21 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
->badge($failedCount)
|
||||
->badgeColor('danger')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q)
|
||||
->whereHas('payments', fn ($sub) => $sub
|
||||
->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 = $this->realScope()
|
||||
->whereDoesntHave('payments', fn ($q) => $q
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()))
|
||||
$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')
|
||||
@@ -144,7 +147,6 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
->badgeColor('gray')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyExpired($q));
|
||||
|
||||
// All real
|
||||
$tabs['all'] = Tab::make('All')
|
||||
->icon('heroicon-o-squares-2x2')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyReal($q));
|
||||
|
||||
@@ -82,7 +82,7 @@ class ScheduledGivingDashboard extends Page
|
||||
$currentIds = $this->currentSeasonIds($c->id);
|
||||
$expiredIds = $realIds->diff($currentIds);
|
||||
|
||||
// Current season payment stats
|
||||
// Current season payment stats — separate due vs future
|
||||
$currentPayments = null;
|
||||
if ($currentIds->isNotEmpty()) {
|
||||
$currentPayments = DB::table('scheduled_giving_payments')
|
||||
@@ -91,10 +91,12 @@ class ScheduledGivingDashboard extends Page
|
||||
->selectRaw("
|
||||
COUNT(*) as total,
|
||||
SUM(is_paid = 1) as paid,
|
||||
SUM(is_paid = 0) as pending,
|
||||
SUM(is_paid = 0 AND attempts > 0) as failed,
|
||||
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 THEN amount ELSE 0 END) as pending_amount,
|
||||
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
|
||||
")
|
||||
@@ -121,6 +123,9 @@ class ScheduledGivingDashboard extends Page
|
||||
$fullyPaid = $row->cnt ?? 0;
|
||||
}
|
||||
|
||||
$due = (int) ($currentPayments->due ?? 0);
|
||||
$paid = (int) ($currentPayments->paid ?? 0);
|
||||
|
||||
$result[] = [
|
||||
'campaign' => $c,
|
||||
'all_time_subscribers' => $realIds->count(),
|
||||
@@ -130,13 +135,16 @@ class ScheduledGivingDashboard extends Page
|
||||
// Current season
|
||||
'current_subscribers' => $currentIds->count(),
|
||||
'expired_subscribers' => $expiredIds->count(),
|
||||
'current_payments' => (int) ($currentPayments->total ?? 0),
|
||||
'current_paid' => (int) ($currentPayments->paid ?? 0),
|
||||
'current_pending' => (int) ($currentPayments->pending ?? 0),
|
||||
'current_failed' => (int) ($currentPayments->failed ?? 0),
|
||||
'current_collected' => ($currentPayments->collected ?? 0) / 100,
|
||||
'current_pending_amount' => ($currentPayments->pending_amount ?? 0) / 100,
|
||||
'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,
|
||||
@@ -169,25 +177,32 @@ class ScheduledGivingDashboard extends Page
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw("
|
||||
SUM(is_paid = 1) as paid,
|
||||
SUM(is_paid = 0 AND attempts > 0) as failed,
|
||||
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 THEN amount ELSE 0 END) / 100 as pending,
|
||||
COUNT(*) as total
|
||||
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),
|
||||
'current_collected' => (float) ($currentStats->collected ?? 0),
|
||||
'current_pending' => (float) ($currentStats->pending ?? 0),
|
||||
'current_failed' => (int) ($currentStats->failed ?? 0),
|
||||
'collection_rate' => ($currentStats->total ?? 0) > 0
|
||||
? round($currentStats->paid / $currentStats->total * 100, 1)
|
||||
: 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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -16,29 +16,33 @@
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-primary-600">{{ number_format($global['current_subscribers']) }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Active This Season</div>
|
||||
<div class="text-xs text-gray-400">{{ number_format($global['expired_subscribers']) }} from past seasons</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Active</div>
|
||||
<div class="text-xs text-gray-400">{{ number_format($global['expired_subscribers']) }} past seasons</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-success-600">£{{ number_format($global['current_collected'], 0) }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Collected This Season</div>
|
||||
<div class="text-xs text-gray-400">£{{ number_format($global['all_time_collected'], 0) }} all-time</div>
|
||||
<div class="text-3xl font-bold text-success-600">£{{ number_format($global['collected'], 0) }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Collected</div>
|
||||
<div class="text-xs text-gray-400">{{ number_format($global['paid_payments']) }}/{{ number_format($global['due_payments']) }} due paid</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-warning-600">£{{ number_format($global['current_pending'], 0) }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Pending</div>
|
||||
@if ($global['current_failed'] > 0)
|
||||
<div class="text-xs text-danger-500">{{ $global['current_failed'] }} failed</div>
|
||||
@endif
|
||||
<div class="text-3xl font-bold {{ $global['failed_count'] > 0 ? 'text-danger-600' : 'text-gray-400' }}">£{{ number_format($global['failed_amount'], 0) }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Failed</div>
|
||||
<div class="text-xs text-danger-500">{{ number_format($global['failed_count']) }} payments</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-gray-500">£{{ number_format($global['scheduled_amount'], 0) }}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Upcoming</div>
|
||||
<div class="text-xs text-gray-400">{{ number_format($global['scheduled_count']) }} not yet due</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold {{ $global['collection_rate'] >= 80 ? 'text-success-600' : ($global['collection_rate'] >= 60 ? 'text-warning-600' : 'text-danger-600') }}">
|
||||
{{ $global['collection_rate'] }}%
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mt-1">Collection Rate</div>
|
||||
<div class="text-xs text-gray-400">of due payments</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@@ -49,8 +53,11 @@
|
||||
@php
|
||||
$c = $data['campaign'];
|
||||
$hasCurrent = $data['current_subscribers'] > 0;
|
||||
$progressPct = $data['current_payments'] > 0
|
||||
? round($data['current_paid'] / $data['current_payments'] * 100)
|
||||
$duePct = $data['due_payments'] > 0
|
||||
? round($data['paid_payments'] / $data['due_payments'] * 100)
|
||||
: 0;
|
||||
$overallPct = $data['total_payments'] > 0
|
||||
? round($data['paid_payments'] / $data['total_payments'] * 100)
|
||||
: 0;
|
||||
@endphp
|
||||
|
||||
@@ -67,8 +74,6 @@
|
||||
</x-slot>
|
||||
|
||||
@if ($hasCurrent)
|
||||
{{-- Current Season --}}
|
||||
<div class="text-xs font-medium text-primary-600 uppercase tracking-wide mb-2">This Season</div>
|
||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">Subscribers</div>
|
||||
@@ -80,23 +85,35 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">Collected</div>
|
||||
<div class="text-lg font-semibold text-success-600">£{{ number_format($data['current_collected'], 0) }}</div>
|
||||
<div class="text-lg font-semibold text-success-600">£{{ number_format($data['collected'], 0) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">Pending</div>
|
||||
<div class="text-lg font-semibold text-warning-600">£{{ number_format($data['current_pending_amount'], 0) }}</div>
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">
|
||||
{{ $data['collection_rate'] }}% Rate
|
||||
</div>
|
||||
<div class="text-lg font-semibold {{ $data['collection_rate'] >= 80 ? 'text-success-600' : ($data['collection_rate'] >= 60 ? 'text-warning-600' : 'text-danger-600') }}">
|
||||
{{ $data['paid_payments'] }}/{{ $data['due_payments'] }} due
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Payment progress bar --}}
|
||||
{{-- Progress bar: paid / total (including future) --}}
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>{{ number_format($data['current_paid']) }} / {{ number_format($data['current_payments']) }} payments</span>
|
||||
<span>{{ $progressPct }}%</span>
|
||||
<span>{{ number_format($data['paid_payments']) }} paid, {{ number_format($data['failed_payments']) }} failed, {{ number_format($data['scheduled_payments']) }} upcoming</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="h-2.5 rounded-full {{ $progressPct >= 80 ? 'bg-success-500' : ($progressPct >= 50 ? 'bg-warning-500' : 'bg-primary-500') }}"
|
||||
style="width: {{ $progressPct }}%"></div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700 flex overflow-hidden">
|
||||
@if ($data['total_payments'] > 0)
|
||||
<div class="h-2.5 bg-success-500" style="width: {{ $data['paid_payments'] / $data['total_payments'] * 100 }}%"></div>
|
||||
<div class="h-2.5 bg-danger-400" style="width: {{ $data['failed_payments'] / $data['total_payments'] * 100 }}%"></div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex justify-between text-xs mt-1">
|
||||
<span class="text-success-600">■ Paid</span>
|
||||
@if ($data['failed_payments'] > 0)
|
||||
<span class="text-danger-500">■ Failed (£{{ number_format($data['failed_amount'], 0) }})</span>
|
||||
@endif
|
||||
<span class="text-gray-400">■ Upcoming (£{{ number_format($data['scheduled_amount'], 0) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,12 +128,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">Failed</div>
|
||||
<div class="font-semibold {{ $data['current_failed'] > 0 ? 'text-danger-600' : 'text-gray-400' }}">{{ $data['current_failed'] }}</div>
|
||||
<div class="font-semibold {{ $data['failed_payments'] > 0 ? 'text-danger-600' : 'text-gray-400' }}">{{ $data['failed_payments'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- All-time summary --}}
|
||||
{{-- All-time --}}
|
||||
<div class="{{ $hasCurrent ? 'mt-3 pt-3 border-t dark:border-gray-700' : '' }}">
|
||||
<div class="text-xs font-medium text-gray-400 uppercase tracking-wide mb-1">All Time</div>
|
||||
<div class="flex justify-between text-sm text-gray-500">
|
||||
@@ -220,7 +237,6 @@
|
||||
<p class="text-sm text-gray-500 mb-3">
|
||||
{{ number_format($quality['total_records']) }} total records in database.
|
||||
Only {{ number_format($global['total_subscribers']) }} are real subscribers with payments.
|
||||
The rest are incomplete sign-ups, test data, or soft-deleted.
|
||||
</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-center text-sm">
|
||||
<div class="p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
|
||||
Reference in New Issue
Block a user