- {tablePledges.map((p: Pledge) => {
- const sl = STATUS[p.status] || STATUS.new
- return (
-
)
}
diff --git a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx
index d3a2e6c..c073ec9 100644
--- a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx
+++ b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx
@@ -186,7 +186,7 @@ export default function SettingsPage() {
: `${totalCount - doneCount} thing${totalCount - doneCount > 1 ? "s" : ""} left before you go live.`
return (
-
+
{/* ── Header — human progress, not a form page ── */}
@@ -210,7 +210,11 @@ export default function SettingsPage() {
{error &&
{error}
}
- {/* ── The Checklist ── */}
+ {/* ━━ TWO-COLUMN: Checklist left, Education right ━━━━━━ */}
+
+
+ {/* LEFT: The Checklist */}
+
{/* ▸ WhatsApp ─────────────────────────── */}
@@ -501,6 +505,85 @@ export default function SettingsPage() {
+
+
+ {/* RIGHT: Education + Context */}
+
+
+ {/* What each setting does */}
+
+
+
What you're setting up
+
+
+ {[
+ { n: "01", title: "WhatsApp", desc: "Scan a QR code to connect your phone. Donors get receipts and reminders automatically. They can reply PAID, HELP, or CANCEL.", essential: true },
+ { n: "02", title: "Bank account", desc: "Your sort code and account number. Shown to donors after they pledge so they know where to send money.", essential: true },
+ { n: "03", title: "Your charity", desc: "Name and brand colour shown on pledge pages. Donors see this when they tap your link.", essential: true },
+ { n: "04", title: "Card payments", desc: "Connect Stripe to let donors pay by Visa, Mastercard, or Apple Pay. Money goes straight to your account.", essential: false },
+ { n: "05", title: "Team", desc: "Invite community leaders and volunteers. They get their own pledge links and can see their own results.", essential: false },
+ ].map(s => (
+
+
{s.n}
+
+
+ {s.title}
+ {s.essential && Required}
+
+
{s.desc}
+
+
+ ))}
+
+
+
+ {/* Privacy & data */}
+
+
Privacy & data
+
+
+ Your data stays yours. We never access your Stripe account, bank details, or WhatsApp messages. Everything is stored encrypted.
+
+
+ GDPR compliant. Donor consent is recorded at pledge time. You can export or delete all data anytime.
+
+
+ No vendor lock-in. Download your full data as CSV from Reports. Your donors, your data, always.
+
+
+
+
+ {/* Common questions */}
+
+
Common questions
+
+ {[
+ { q: "Do I need Stripe?", a: "No — most charities use bank transfer only. Stripe is optional for orgs that want card payments." },
+ { q: "Can I change my bank details later?", a: "Yes. New pledges will show the updated details. Existing pledges keep the original reference." },
+ { q: "What happens if WhatsApp disconnects?", a: "Reminders pause until you reconnect. Come back here, scan the QR again. It takes 30 seconds." },
+ { q: "Can volunteers see financial data?", a: "No. Volunteers only see their own link performance. Admins see everything." },
+ ].map(item => (
+
+ ))}
+
+
+
+ {/* Need help? */}
+
+
Need help setting up?
+
+ Our team can walk you through the setup in 15 minutes. Free, no strings attached.
+
+
+ Get in touch →
+
+
+
+
+
)
}
diff --git a/temp_files/sg/ListScheduledGivingDonations.php b/temp_files/sg/ListScheduledGivingDonations.php
new file mode 100644
index 0000000..ed09505
--- /dev/null
+++ b/temp_files/sg/ListScheduledGivingDonations.php
@@ -0,0 +1,96 @@
+count();
+ return "{$active} active subscribers across all campaigns.";
+ }
+
+ public function getTabs(): array
+ {
+ $campaigns = ScheduledGivingCampaign::withCount([
+ 'donations as active_count' => fn ($q) => $q->where('is_active', true),
+ ])->get();
+
+ $tabs = [];
+
+ // All active tab first
+ $totalActive = ScheduledGivingDonation::where('is_active', true)->count();
+ $tabs['active'] = Tab::make('All Active')
+ ->icon('heroicon-o-check-circle')
+ ->badge($totalActive)
+ ->badgeColor('success')
+ ->modifyQueryUsing(fn (Builder $q) => $q->where('is_active', true));
+
+ // One tab per campaign
+ foreach ($campaigns as $c) {
+ $slug = str($c->title)->slug()->toString();
+ $tabs[$slug] = Tab::make($c->title)
+ ->icon('heroicon-o-calendar')
+ ->badge($c->active_count > 0 ? $c->active_count : null)
+ ->badgeColor($c->active ? 'primary' : 'gray')
+ ->modifyQueryUsing(fn (Builder $q) => $q
+ ->where('scheduled_giving_campaign_id', $c->id)
+ ->where('is_active', true));
+ }
+
+ // Failed payments tab
+ $failedCount = ScheduledGivingDonation::where('is_active', true)
+ ->whereHas('payments', fn ($q) => $q
+ ->where('is_paid', false)
+ ->where('attempts', '>', 0))
+ ->count();
+
+ $tabs['failed'] = Tab::make('Failed Payments')
+ ->icon('heroicon-o-exclamation-triangle')
+ ->badge($failedCount > 0 ? $failedCount : null)
+ ->badgeColor('danger')
+ ->modifyQueryUsing(fn (Builder $q) => $q
+ ->where('is_active', true)
+ ->whereHas('payments', fn ($sub) => $sub
+ ->where('is_paid', false)
+ ->where('attempts', '>', 0)));
+
+ // Cancelled
+ $cancelled = ScheduledGivingDonation::where('is_active', false)->count();
+ $tabs['cancelled'] = Tab::make('Cancelled')
+ ->icon('heroicon-o-x-circle')
+ ->badge($cancelled > 0 ? $cancelled : null)
+ ->badgeColor('gray')
+ ->modifyQueryUsing(fn (Builder $q) => $q->where('is_active', false));
+
+ // Everything
+ $tabs['all'] = Tab::make('Everything')
+ ->icon('heroicon-o-squares-2x2');
+
+ return $tabs;
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [];
+ }
+
+ public function getDefaultActiveTab(): string | int | null
+ {
+ return 'active';
+ }
+}
diff --git a/temp_files/sg/ScheduledGivingDashboard.php b/temp_files/sg/ScheduledGivingDashboard.php
new file mode 100644
index 0000000..be60504
--- /dev/null
+++ b/temp_files/sg/ScheduledGivingDashboard.php
@@ -0,0 +1,204 @@
+ fn ($q) => $q->where('is_active', true),
+ 'donations as cancelled_count' => fn ($q) => $q->where('is_active', false),
+ ])->get();
+
+ $result = [];
+
+ foreach ($campaigns as $c) {
+ $donationIds = $c->donations()->pluck('id');
+
+ if ($donationIds->isEmpty()) {
+ $result[] = [
+ 'campaign' => $c,
+ 'subscribers' => 0,
+ 'active' => 0,
+ 'cancelled' => 0,
+ 'total_payments' => 0,
+ 'paid_payments' => 0,
+ 'pending_payments' => 0,
+ 'failed_payments' => 0,
+ 'collected' => 0,
+ 'pending_amount' => 0,
+ 'avg_per_night' => 0,
+ 'completion_rate' => 0,
+ 'dates' => $c->dates ?? [],
+ 'next_payment' => null,
+ 'last_payment' => null,
+ ];
+ continue;
+ }
+
+ $payments = DB::table('scheduled_giving_payments')
+ ->whereIn('scheduled_giving_donation_id', $donationIds)
+ ->whereNull('deleted_at')
+ ->selectRaw("
+ COUNT(*) as total,
+ SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) as paid,
+ SUM(CASE WHEN is_paid = 0 THEN 1 ELSE 0 END) as pending,
+ SUM(CASE WHEN is_paid = 0 AND attempts > 0 THEN 1 ELSE 0 END) as failed,
+ 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,
+ 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,
+ MAX(CASE WHEN is_paid = 1 THEN expected_at ELSE NULL END) as last_payment
+ ")
+ ->first();
+
+ // Completion: subscribers who paid ALL their payments
+ $totalNights = count($c->dates ?? []);
+ $fullyPaid = 0;
+ if ($totalNights > 0 && $donationIds->isNotEmpty()) {
+ $ids = $donationIds->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;
+ }
+
+ $result[] = [
+ 'campaign' => $c,
+ 'subscribers' => $c->donations_count,
+ 'active' => $c->active_count,
+ 'cancelled' => $c->cancelled_count,
+ 'total_payments' => (int) $payments->total,
+ 'paid_payments' => (int) $payments->paid,
+ 'pending_payments' => (int) $payments->pending,
+ 'failed_payments' => (int) $payments->failed,
+ 'collected' => ($payments->collected ?? 0) / 100,
+ 'pending_amount' => ($payments->pending_amount ?? 0) / 100,
+ 'avg_per_night' => ($payments->avg_amount ?? 0) / 100,
+ 'completion_rate' => $c->active_count > 0
+ ? round($fullyPaid / $c->active_count * 100, 1)
+ : 0,
+ 'fully_completed' => $fullyPaid,
+ 'dates' => $c->dates ?? [],
+ 'total_nights' => $totalNights,
+ 'next_payment' => $payments->next_payment,
+ 'last_payment' => $payments->last_payment,
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Global totals across all campaigns.
+ */
+ public function getGlobalStats(): array
+ {
+ $row = DB::table('scheduled_giving_payments')
+ ->whereNull('deleted_at')
+ ->selectRaw("
+ COUNT(*) as total,
+ SUM(CASE WHEN is_paid = 1 THEN 1 ELSE 0 END) as paid,
+ SUM(CASE WHEN is_paid = 0 AND attempts > 0 THEN 1 ELSE 0 END) as failed,
+ 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
+ ")
+ ->first();
+
+ $totalSubs = ScheduledGivingDonation::count();
+ $activeSubs = ScheduledGivingDonation::where('is_active', true)->count();
+
+ return [
+ 'total_subscribers' => $totalSubs,
+ 'active_subscribers' => $activeSubs,
+ 'total_payments' => (int) ($row->total ?? 0),
+ 'paid_payments' => (int) ($row->paid ?? 0),
+ 'failed_payments' => (int) ($row->failed ?? 0),
+ 'collected' => (float) ($row->collected ?? 0),
+ 'pending' => (float) ($row->pending ?? 0),
+ 'collection_rate' => $row->total > 0
+ ? round($row->paid / $row->total * 100, 1)
+ : 0,
+ ];
+ }
+
+ /**
+ * Recent failed payments needing attention.
+ */
+ public function getFailedPayments(): array
+ {
+ 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')
+ ->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();
+ }
+
+ /**
+ * Upcoming payments in the next 48 hours.
+ */
+ public function getUpcomingPayments(): array
+ {
+ 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')
+ ->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();
+ }
+}
diff --git a/temp_files/sg/nav_update.py b/temp_files/sg/nav_update.py
new file mode 100644
index 0000000..b359220
--- /dev/null
+++ b/temp_files/sg/nav_update.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+"""
+Navigation overhaul:
+- Daily: Donors, Donations (with recurring tab)
+- Seasonal Campaigns: Campaign Dashboard, Subscribers, Giving Campaigns config
+- Fundraising: Review Queue, All Fundraisers
+- Setup: unchanged
+"""
+import os
+
+BASE = '/home/forge/app.charityright.org.uk'
+
+# ── 1. ScheduledGivingDonationResource → Seasonal Campaigns group ──
+
+path = os.path.join(BASE, 'app/Filament/Resources/ScheduledGivingDonationResource.php')
+with open(path, 'r') as f:
+ c = f.read()
+
+# Change nav group
+c = c.replace(
+ "protected static ?string $navigationGroup = 'Daily';",
+ "protected static ?string $navigationGroup = 'Seasonal Campaigns';"
+)
+# Change label
+c = c.replace(
+ "protected static ?string $navigationLabel = 'Regular Giving';",
+ "protected static ?string $navigationLabel = 'Subscribers';"
+)
+# Change sort
+c = c.replace(
+ "protected static ?int $navigationSort = 3;",
+ "protected static ?int $navigationSort = 1;"
+)
+# Change model label
+c = c.replace(
+ "protected static ?string $modelLabel = 'Regular Giving';",
+ "protected static ?string $modelLabel = 'Scheduled Giving';"
+)
+c = c.replace(
+ "protected static ?string $pluralModelLabel = 'Regular Giving';",
+ "protected static ?string $pluralModelLabel = 'Scheduled Giving';"
+)
+# Change icon to calendar
+c = c.replace(
+ "protected static ?string $navigationIcon = 'heroicon-o-arrow-path';",
+ "protected static ?string $navigationIcon = 'heroicon-o-user-group';"
+)
+
+with open(path, 'w') as f:
+ f.write(c)
+print('Updated ScheduledGivingDonationResource nav')
+
+# ── 2. ScheduledGivingCampaignResource → Seasonal Campaigns group ──
+
+path = os.path.join(BASE, 'app/Filament/Resources/ScheduledGivingCampaignResource.php')
+with open(path, 'r') as f:
+ c = f.read()
+
+c = c.replace(
+ "protected static ?string $navigationGroup = 'Fundraising';",
+ "protected static ?string $navigationGroup = 'Seasonal Campaigns';"
+)
+c = c.replace(
+ "protected static ?string $navigationLabel = 'Giving Campaigns';",
+ "protected static ?string $navigationLabel = 'Campaign Config';"
+)
+
+# Add sort if missing
+if 'navigationSort' not in c:
+ c = c.replace(
+ "protected static ?string $navigationLabel = 'Campaign Config';",
+ "protected static ?string $navigationLabel = 'Campaign Config';\n\n protected static ?int $navigationSort = 2;"
+ )
+
+with open(path, 'w') as f:
+ f.write(c)
+print('Updated ScheduledGivingCampaignResource nav')
+
+# ── 3. AdminPanelProvider → Add Seasonal Campaigns group ──
+
+path = os.path.join(BASE, 'app/Providers/Filament/AdminPanelProvider.php')
+with open(path, 'r') as f:
+ c = f.read()
+
+old_groups = """ ->navigationGroups([
+ // ── Daily Work (always visible, top of sidebar) ──
+ NavigationGroup::make('Daily')
+ ->collapsible(false),
+
+ // ── Fundraising (campaigns, review queue) ──
+ NavigationGroup::make('Fundraising')
+ ->icon('heroicon-o-megaphone')
+ ->collapsible(),
+
+ // ── Setup (rarely touched config) ──
+ NavigationGroup::make('Setup')
+ ->icon('heroicon-o-cog-6-tooth')
+ ->collapsible()
+ ->collapsed(),
+ ])"""
+
+new_groups = """ ->navigationGroups([
+ // ── Daily Work (always visible, top of sidebar) ──
+ NavigationGroup::make('Daily')
+ ->collapsible(false),
+
+ // ── Seasonal Campaigns (30 Nights, 10 Days, Night of Power) ──
+ NavigationGroup::make('Seasonal Campaigns')
+ ->icon('heroicon-o-calendar-days')
+ ->collapsible(),
+
+ // ── Fundraising (appeals, review queue) ──
+ NavigationGroup::make('Fundraising')
+ ->icon('heroicon-o-megaphone')
+ ->collapsible(),
+
+ // ── Setup (rarely touched config) ──
+ NavigationGroup::make('Setup')
+ ->icon('heroicon-o-cog-6-tooth')
+ ->collapsible()
+ ->collapsed(),
+ ])"""
+
+c = c.replace(old_groups, new_groups)
+
+with open(path, 'w') as f:
+ f.write(c)
+print('Updated AdminPanelProvider nav groups')
+
+# ── 4. Update ListScheduledGivingDonations labels ──
+
+path = os.path.join(BASE, 'app/Filament/Resources/ScheduledGivingDonationResource/Pages/ListScheduledGivingDonations.php')
+with open(path, 'r') as f:
+ c = f.read()
+
+c = c.replace(
+ "return 'Regular Giving';",
+ "return 'Scheduled Giving Subscribers';"
+)
+c = c.replace(
+ 'return "{$active} people giving every month.";',
+ 'return "{$active} active subscribers across all campaigns.";'
+)
+
+with open(path, 'w') as f:
+ f.write(c)
+print('Updated ListScheduledGivingDonations labels')
+
+# ── 5. Add "Recurring" tab to Donations list ──
+# Monthly/weekly donors should be visible under Donations
+
+path = os.path.join(BASE, 'app/Filament/Resources/DonationResource/Pages/ListDonations.php')
+with open(path, 'r') as f:
+ c = f.read()
+
+# Check if 'recurring' tab already exists
+if "'recurring'" not in c:
+ # Find the 'everything' tab and add 'recurring' before it
+ old_everything = """ 'everything' => Tab::make('Everything')"""
+ new_recurring = """ 'recurring' => Tab::make('Recurring')
+ ->icon('heroicon-o-arrow-path')
+ ->badge($recurring > 0 ? $recurring : null)
+ ->badgeColor('info')
+ ->modifyQueryUsing(fn (Builder $q) => $q->where('reoccurrence', '!=', -1)
+ ->whereHas('donationConfirmation', fn ($sub) => $sub->whereNotNull('confirmed_at'))),
+
+ 'everything' => Tab::make('Everything')"""
+ c = c.replace(old_everything, new_recurring)
+
+ # Add $recurring count variable
+ if '$recurring' not in c:
+ c = c.replace(
+ "return [",
+ "$recurring = Donation::where('reoccurrence', '!=', -1)\n ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))\n ->count();\n\n return [",
+ 1 # only first occurrence in getTabs
+ )
+
+ with open(path, 'w') as f:
+ f.write(c)
+ print('Added Recurring tab to ListDonations')
+else:
+ print('ListDonations already has recurring tab')
+
+print('Navigation update complete!')
diff --git a/temp_files/sg/scheduled-giving-dashboard.blade.php b/temp_files/sg/scheduled-giving-dashboard.blade.php
new file mode 100644
index 0000000..4af0184
--- /dev/null
+++ b/temp_files/sg/scheduled-giving-dashboard.blade.php
@@ -0,0 +1,199 @@
+
+ @php
+ $global = $this->getGlobalStats();
+ $campaigns = $this->getCampaignData();
+ $failed = $this->getFailedPayments();
+ $upcoming = $this->getUpcomingPayments();
+ @endphp
+
+ {{-- ── Global Overview ──────────────────────────────────────── --}}
+
+
+
+
{{ number_format($global['active_subscribers']) }}
+
Active Subscribers
+
+
+
+
+
+
£{{ number_format($global['collected'], 2) }}
+
Total Collected
+
+
+
+
+
+
£{{ number_format($global['pending'], 2) }}
+
Pending Collection
+
+
+
+
+
+
+ {{ $global['collection_rate'] }}%
+
+
Collection Rate
+
+
+
+
+ {{-- ── Campaign Cards ──────────────────────────────────────── --}}
+
+ @foreach ($campaigns as $data)
+ @php
+ $c = $data['campaign'];
+ $isActive = $c->active;
+ $progressPct = $data['total_payments'] > 0
+ ? round($data['paid_payments'] / $data['total_payments'] * 100)
+ : 0;
+ @endphp
+
+
+
+
+ {{ $c->title }}
+ @if ($isActive)
+ ● Live
+ @else
+ ○ Ended
+ @endif
+
+
+
+ {{-- Stats grid --}}
+
+
+
Subscribers
+
{{ number_format($data['active']) }} / {{ number_format($data['subscribers']) }}
+
+
+
Avg / Night
+
£{{ number_format($data['avg_per_night'], 2) }}
+
+
+
Collected
+
£{{ number_format($data['collected'], 2) }}
+
+
+
Pending
+
£{{ number_format($data['pending_amount'], 2) }}
+
+
+
+ {{-- Payment progress bar --}}
+
+
+ {{ number_format($data['paid_payments']) }} / {{ number_format($data['total_payments']) }} payments
+ {{ $progressPct }}%
+
+
+
+
+ {{-- Key metrics --}}
+
+
+
Nights
+
{{ $data['total_nights'] }}
+
+
+
Completed
+
{{ $data['fully_completed'] ?? 0 }}
+
+
+
Failed
+
{{ $data['failed_payments'] }}
+
+
+
+ {{-- Quick links --}}
+
+
+ @endforeach
+
+
+ {{-- ── Upcoming Payments ───────────────────────────────────── --}}
+ @if (count($upcoming) > 0)
+
+
+
+
+ Upcoming Payments (Next 48 Hours)
+
+
+
+
+ @foreach ($upcoming as $u)
+
+
+
{{ $u->campaign }}
+
{{ $u->payment_count }} payments
+
+
+
£{{ number_format($u->total_amount, 2) }}
+
{{ \Carbon\Carbon::parse($u->earliest)->diffForHumans() }}
+
+
+ @endforeach
+
+
+ @endif
+
+ {{-- ── Failed Payments (Needs Attention) ───────────────────── --}}
+ @if (count($failed) > 0)
+
+
+
+
+ Failed Payments — Needs Attention ({{ count($failed) }})
+
+
+
+
+
+
+
+ | Donor |
+ Campaign |
+ Amount |
+ Expected |
+ Attempts |
+ |
+
+
+
+ @foreach ($failed as $f)
+
+ |
+ {{ $f->donor_name }}
+ {{ $f->donor_email }}
+ |
+ {{ $f->campaign }} |
+ £{{ number_format($f->amount / 100, 2) }} |
+ {{ \Carbon\Carbon::parse($f->expected_at)->format('d M Y') }} |
+
+
+ {{ $f->attempts }}x failed
+
+ |
+
+ View →
+ |
+
+ @endforeach
+
+
+
+
+ @endif
+
diff --git a/temp_files/sg/table_update.py b/temp_files/sg/table_update.py
new file mode 100644
index 0000000..9066b09
--- /dev/null
+++ b/temp_files/sg/table_update.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+"""
+Redesign the ScheduledGivingDonationResource table columns.
+Replace the old columns with campaign-aware, care-focused columns.
+"""
+import os
+
+BASE = '/home/forge/app.charityright.org.uk'
+path = os.path.join(BASE, 'app/Filament/Resources/ScheduledGivingDonationResource.php')
+
+with open(path, 'r') as f:
+ c = f.read()
+
+old_columns = """ ->columns([
+ IconColumn::make('donationConfirmation.confirmed_at')
+ ->label('Confirmed ?')
+ ->searchable()
+ ->boolean(),
+
+ TextColumn::make('total_amount')
+ ->label('Total')
+ ->numeric()
+ ->money('gbp')
+ ->searchable()
+ ->sortable(),
+
+ TextColumn::make('amount_admin')
+ ->label('Admin')
+ ->numeric()
+ ->money('gbp')
+ ->searchable()
+ ->sortable(),
+
+ TextColumn::make('customer.name')
+ ->label('Customer')
+ ->description(fn (ScheduledGivingDonation $donation) => $donation->customer?->email)
+ ->sortable()
+ ->searchable(true, function (Builder $query, string $search) {
+ $query
+ ->whereHas('customer', function (Builder $query) use ($search) {
+ $query
+ ->where('first_name', 'like', "%{$search}%")
+ ->orWhere('last_name', 'like', "%{$search}%")
+ ->orWhere('email', 'like', "%{$search}%");
+ });
+ }),
+
+ TextColumn::make('provider_reference')
+ ->searchable()
+ ->label('Provider Reference'),
+
+ TextColumn::make('appeal.name')
+ ->numeric()
+ ->toggleable(isToggledHiddenByDefault: true)
+ ->sortable(),
+
+ TextColumn::make('created_at')
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('updated_at')
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])"""
+
+new_columns = """ ->columns([
+ IconColumn::make('is_active')
+ ->label('')
+ ->boolean()
+ ->trueIcon('heroicon-o-check-circle')
+ ->falseIcon('heroicon-o-x-circle')
+ ->trueColor('success')
+ ->falseColor('danger')
+ ->tooltip(fn (ScheduledGivingDonation $r) => $r->is_active ? 'Active' : 'Cancelled'),
+
+ TextColumn::make('customer.name')
+ ->label('Donor')
+ ->description(fn (ScheduledGivingDonation $d) => $d->customer?->email)
+ ->sortable()
+ ->searchable(query: function (Builder $query, string $search) {
+ $query->whereHas('customer', fn (Builder $q) => $q
+ ->where('first_name', 'like', "%{$search}%")
+ ->orWhere('last_name', 'like', "%{$search}%")
+ ->orWhere('email', 'like', "%{$search}%"));
+ }),
+
+ TextColumn::make('scheduledGivingCampaign.title')
+ ->label('Campaign')
+ ->badge()
+ ->color(fn ($state) => match ($state) {
+ '30 Nights of Giving' => 'primary',
+ '10 Days of Giving' => 'success',
+ 'Night of Power' => 'warning',
+ default => 'gray',
+ }),
+
+ TextColumn::make('total_amount')
+ ->label('Per Night')
+ ->money('gbp', divideBy: 100)
+ ->sortable(),
+
+ TextColumn::make('payments_count')
+ ->label('Progress')
+ ->counts('payments')
+ ->formatStateUsing(function ($state, ScheduledGivingDonation $record) {
+ $paid = $record->payments()->where('is_paid', true)->count();
+ $total = $state;
+ if ($total === 0) return 'No payments';
+ $pct = round($paid / $total * 100);
+ return "{$paid}/{$total} ({$pct}%)";
+ })
+ ->color(function (ScheduledGivingDonation $record) {
+ $total = $record->payments()->count();
+ if ($total === 0) return 'gray';
+ $paid = $record->payments()->where('is_paid', true)->count();
+ $pct = $paid / $total * 100;
+ return $pct >= 80 ? 'success' : ($pct >= 40 ? 'warning' : 'danger');
+ })
+ ->badge(),
+
+ TextColumn::make('created_at')
+ ->label('Signed Up')
+ ->date('d M Y')
+ ->description(fn (ScheduledGivingDonation $d) => $d->created_at?->diffForHumans())
+ ->sortable(),
+
+ TextColumn::make('reference_code')
+ ->label('Ref')
+ ->searchable()
+ ->fontFamily('mono')
+ ->copyable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])"""
+
+c = c.replace(old_columns, new_columns)
+
+with open(path, 'w') as f:
+ f.write(c)
+print('Table columns updated')