Stripe integration: charity connects their own Stripe account
Model: PNPL never touches the money. Each charity connects their own Stripe account by pasting their API key in Settings. When a donor chooses card payment, they're redirected to Stripe Checkout. The money lands in the charity's Stripe balance. ## Schema - Organization.stripeSecretKey (new column) - Organization.stripeWebhookSecret (new column) ## New/rewritten files - src/lib/stripe.ts — getStripeForOrg(secretKey), per-org client - src/app/api/stripe/checkout/route.ts — uses org's key, not env var - src/app/api/stripe/webhook/route.ts — tries all org webhook secrets - src/app/p/[token]/steps/card-payment-step.tsx — redirect to Stripe Checkout (no fake card form — Stripe handles PCI) ## Settings page - New 'Card payments' section between Bank and Charity - Instructions: how to get your Stripe API key - Webhook setup in collapsed <details> (optional, for auto-confirm) - 'Card payments live' green banner when connected - Readiness bar shows Stripe status (5 columns now) ## Pledge flow - PaymentStep shows card option ONLY if org has Stripe configured - hasStripe flag passed from /api/qr/[token] → PaymentStep - Secret key never exposed to frontend (only boolean hasStripe) ## How it works 1. Charity pastes sk_live_... in Settings → Save 2. Donor opens pledge link → sees 'Bank Transfer', 'Direct Debit', 'Card' 3. Donor picks card → enters name + email → redirects to Stripe Checkout 4. Stripe processes payment → money in charity's Stripe balance 5. (Optional) Webhook auto-confirms pledge as paid Payment options: - Bank Transfer: zero fees (default, always available) - Direct Debit via GoCardless: 1% + 20p (if org configured) - Card via Stripe: standard Stripe fees (if org configured)
This commit is contained in:
@@ -27,9 +27,6 @@ use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Enums\FiltersLayout;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -310,47 +307,7 @@ class AppealResource extends Resource
|
||||
->sortable()
|
||||
->description(fn (Appeal $a) => $a->created_at?->format('d M Y')),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('nurture_segment')
|
||||
->label('Nurture Segment')
|
||||
->options([
|
||||
'needs_outreach' => '🔴 Needs Outreach (£0 raised, 7+ days)',
|
||||
'almost_there' => '🟡 Almost There (80%+ of target)',
|
||||
'target_reached' => '🟢 Target Reached',
|
||||
'slowing' => '🟠 Slowing Down (raised something, 30+ days)',
|
||||
'new_this_week' => '🆕 New This Week',
|
||||
])
|
||||
->query(function (Builder $query, array $data) {
|
||||
if (!$data['value']) return;
|
||||
$query->where('status', 'confirmed')->where('is_accepting_donations', true);
|
||||
|
||||
match ($data['value']) {
|
||||
'needs_outreach' => $query->where('amount_raised', 0)->where('created_at', '<', now()->subDays(7)),
|
||||
'almost_there' => $query->where('amount_raised', '>', 0)->whereRaw('amount_raised >= amount_to_raise * 0.8')->whereRaw('amount_raised < amount_to_raise'),
|
||||
'target_reached' => $query->whereRaw('amount_raised >= amount_to_raise')->where('amount_raised', '>', 0),
|
||||
'slowing' => $query->where('amount_raised', '>', 0)->whereRaw('amount_raised < amount_to_raise * 0.8')->where('created_at', '<', now()->subDays(30)),
|
||||
'new_this_week' => $query->where('created_at', '>=', now()->subDays(7)),
|
||||
default => null,
|
||||
};
|
||||
}),
|
||||
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'confirmed' => 'Live',
|
||||
'pending' => 'Pending Review',
|
||||
]),
|
||||
|
||||
Filter::make('accepting_donations')
|
||||
->label('Currently accepting donations')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->where('is_accepting_donations', true))
|
||||
->default(),
|
||||
|
||||
Filter::make('has_raised')
|
||||
->label('Has raised money')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->where('amount_raised', '>', 0)),
|
||||
], layout: FiltersLayout::AboveContentCollapsible)
|
||||
->filters([])
|
||||
->actions([
|
||||
Action::make('view_page')
|
||||
->label('View Page')
|
||||
|
||||
@@ -17,10 +17,8 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\GlobalSearch\Actions\Action as GlobalSearchAction;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -207,56 +205,7 @@ class CustomerResource extends Resource
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Filter::make('has_donations')
|
||||
->label('Has donated')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->has('donations')),
|
||||
|
||||
Filter::make('monthly_supporter')
|
||||
->label('Monthly supporter')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas(
|
||||
'scheduledGivingDonations',
|
||||
fn ($q2) => $q2->where('is_active', true)
|
||||
)),
|
||||
|
||||
Filter::make('gift_aid')
|
||||
->label('Gift Aid donors')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas(
|
||||
'donations',
|
||||
fn ($q2) => $q2->whereHas('donationPreferences', fn ($q3) => $q3->where('is_gift_aid', true))
|
||||
)),
|
||||
|
||||
Filter::make('major_donor')
|
||||
->label('Major donors (£1000+)')
|
||||
->toggle()
|
||||
->query(function (Builder $q) {
|
||||
$q->whereIn('id', function ($sub) {
|
||||
$sub->select('customer_id')
|
||||
->from('donations')
|
||||
->join('donation_confirmations', 'donations.id', '=', 'donation_confirmations.donation_id')
|
||||
->whereNotNull('donation_confirmations.confirmed_at')
|
||||
->groupBy('customer_id')
|
||||
->havingRaw('SUM(donations.amount) >= 100000');
|
||||
});
|
||||
}),
|
||||
|
||||
Filter::make('incomplete_donations')
|
||||
->label('Has incomplete donations')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas(
|
||||
'donations',
|
||||
fn ($q2) => $q2->whereDoesntHave('donationConfirmation', fn ($q3) => $q3->whereNotNull('confirmed_at'))
|
||||
->where('created_at', '>=', now()->subDays(30))
|
||||
)),
|
||||
|
||||
Filter::make('recent')
|
||||
->label('Joined last 30 days')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->where('created_at', '>=', now()->subDays(30))),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
EditAction::make()
|
||||
->label('Open')
|
||||
|
||||
@@ -34,10 +34,8 @@ use Filament\Tables\Actions\ExportAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Enums\FiltersLayout;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -302,7 +300,7 @@ class DonationResource extends Resource
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters(static::getTableFilters(), layout: FiltersLayout::AboveContentCollapsible)
|
||||
->filters(static::getTableFilters())
|
||||
->actions(static::getTableRowActions())
|
||||
->bulkActions(static::getBulkActions())
|
||||
->headerActions([
|
||||
@@ -431,27 +429,11 @@ class DonationResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Filters ─────────────────────────────────────────────────
|
||||
// Designed around real questions:
|
||||
// "Show me today's incomplete donations" (investigating failures)
|
||||
// "Show me Zakat donations this month" (reporting)
|
||||
// "Show me donations to a specific cause" (allocation check)
|
||||
// ─── Filters (drill-down within tabs) ───────────────────────
|
||||
|
||||
private static function getTableFilters(): array
|
||||
{
|
||||
return [
|
||||
TernaryFilter::make('confirmed')
|
||||
->label('Payment Status')
|
||||
->placeholder('All')
|
||||
->trueLabel('Confirmed only')
|
||||
->falseLabel('Incomplete only')
|
||||
->queries(
|
||||
true: fn (Builder $q) => $q->whereHas('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')),
|
||||
false: fn (Builder $q) => $q->whereDoesntHave('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')),
|
||||
blank: fn (Builder $q) => $q,
|
||||
)
|
||||
->default(true),
|
||||
|
||||
SelectFilter::make('donation_type_id')
|
||||
->label('Cause')
|
||||
->options(fn () => DonationType::orderBy('display_name')->pluck('display_name', 'id'))
|
||||
@@ -473,21 +455,6 @@ class DonationResource extends Resource
|
||||
->when($data['to'], fn (Builder $q) => $q->whereDate('created_at', '<=', $data['to']));
|
||||
})
|
||||
->columns(2),
|
||||
|
||||
Filter::make('is_zakat')
|
||||
->label('Zakat')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas('donationPreferences', fn ($q2) => $q2->where('is_zakat', true))),
|
||||
|
||||
Filter::make('is_gift_aid')
|
||||
->label('Gift Aid')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas('donationPreferences', fn ($q2) => $q2->where('is_gift_aid', true))),
|
||||
|
||||
Filter::make('has_fundraiser')
|
||||
->label('Via Fundraiser')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereNotNull('appeal_id')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
88
temp_files/v4/AdminPanelProvider.php
Normal file
88
temp_files/v4/AdminPanelProvider.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Helpers;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Navigation\NavigationGroup;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->login()
|
||||
->colors(['primary' => config('branding.colours')])
|
||||
->viteTheme('resources/css/filament/admin/theme.css')
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
->sidebarWidth('16rem')
|
||||
->globalSearch(true)
|
||||
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
||||
->globalSearchDebounce('300ms')
|
||||
->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(),
|
||||
])
|
||||
->brandLogo(Helpers::getCurrentLogo(true))
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||
->pages([\Filament\Pages\Dashboard::class])
|
||||
->userMenuItems([
|
||||
'profile' => MenuItem::make()
|
||||
->label('Edit profile')
|
||||
->url(url('user/profile')),
|
||||
|
||||
'back2site' => MenuItem::make()
|
||||
->label('Return to site')
|
||||
->icon('heroicon-o-home')
|
||||
->url(url('/')),
|
||||
])
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
])
|
||||
->login(null)
|
||||
->registration(null)
|
||||
->darkMode(false)
|
||||
->databaseNotifications();
|
||||
}
|
||||
}
|
||||
86
temp_files/v4/ListAppeals.php
Normal file
86
temp_files/v4/ListAppeals.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\AppealResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AppealResource;
|
||||
use App\Models\Appeal;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ListAppeals extends ListRecords
|
||||
{
|
||||
protected static string $resource = AppealResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Fundraisers';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$live = Appeal::where('status', 'confirmed')->where('is_accepting_donations', true)->count();
|
||||
return "{$live} fundraisers are live right now.";
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$needsHelp = Appeal::where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
->where('amount_raised', 0)
|
||||
->where('created_at', '<', now()->subDays(7))
|
||||
->where('created_at', '>', now()->subDays(90))
|
||||
->count();
|
||||
|
||||
$almostThere = Appeal::where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
->where('amount_raised', '>', 0)
|
||||
->whereRaw('amount_raised >= amount_to_raise * 0.8')
|
||||
->whereRaw('amount_raised < amount_to_raise')
|
||||
->count();
|
||||
|
||||
return [
|
||||
'live' => Tab::make('Live')
|
||||
->icon('heroicon-o-signal')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q
|
||||
->where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
),
|
||||
|
||||
'needs_help' => Tab::make('Needs Outreach')
|
||||
->icon('heroicon-o-hand-raised')
|
||||
->badge($needsHelp > 0 ? $needsHelp : null)
|
||||
->badgeColor('danger')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q
|
||||
->where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
->where('amount_raised', 0)
|
||||
->where('created_at', '<', now()->subDays(7))
|
||||
->where('created_at', '>', now()->subDays(90))
|
||||
),
|
||||
|
||||
'almost' => Tab::make('Almost There')
|
||||
->icon('heroicon-o-fire')
|
||||
->badge($almostThere > 0 ? $almostThere : null)
|
||||
->badgeColor('warning')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q
|
||||
->where('status', 'confirmed')
|
||||
->where('is_accepting_donations', true)
|
||||
->where('amount_raised', '>', 0)
|
||||
->whereRaw('amount_raised >= amount_to_raise * 0.8')
|
||||
->whereRaw('amount_raised < amount_to_raise')
|
||||
),
|
||||
|
||||
'hit_target' => Tab::make('Target Reached')
|
||||
->icon('heroicon-o-trophy')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q
|
||||
->where('status', 'confirmed')
|
||||
->where('amount_raised', '>', 0)
|
||||
->whereRaw('amount_raised >= amount_to_raise')
|
||||
),
|
||||
|
||||
'all' => Tab::make('Everything')
|
||||
->icon('heroicon-o-squares-2x2'),
|
||||
];
|
||||
}
|
||||
}
|
||||
50
temp_files/v4/ListApprovalQueues.php
Normal file
50
temp_files/v4/ListApprovalQueues.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ApprovalQueueResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ApprovalQueueResource;
|
||||
use App\Models\ApprovalQueue;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ListApprovalQueues extends ListRecords
|
||||
{
|
||||
protected static string $resource = ApprovalQueueResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Fundraiser Review';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$pending = ApprovalQueue::where('status', 'pending')->count();
|
||||
if ($pending === 0) return 'All caught up — no fundraisers waiting for review.';
|
||||
return "{$pending} fundraisers waiting for your review.";
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$pending = ApprovalQueue::where('status', 'pending')->count();
|
||||
|
||||
return [
|
||||
'pending' => Tab::make('Needs Review')
|
||||
->icon('heroicon-o-clock')
|
||||
->badge($pending > 0 ? $pending : null)
|
||||
->badgeColor('danger')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q->where('status', 'pending')),
|
||||
|
||||
'approved' => Tab::make('Approved')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q->where('status', 'confirmed')),
|
||||
|
||||
'rejected' => Tab::make('Rejected')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q->where('status', 'change_requested')),
|
||||
|
||||
'all' => Tab::make('All')
|
||||
->icon('heroicon-o-squares-2x2'),
|
||||
];
|
||||
}
|
||||
}
|
||||
62
temp_files/v4/ListCustomers.php
Normal file
62
temp_files/v4/ListCustomers.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CustomerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CustomerResource;
|
||||
use App\Models\Customer;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ListCustomers extends ListRecords
|
||||
{
|
||||
protected static string $resource = CustomerResource::class;
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Donors';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
return 'Search by name, email, or phone number. Click a donor to see their full history.';
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
return [
|
||||
'all' => Tab::make('All Donors')
|
||||
->icon('heroicon-o-users'),
|
||||
|
||||
'monthly' => Tab::make('Monthly Supporters')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereHas('scheduledGivingDonations', fn ($q) => $q->where('is_active', true))
|
||||
),
|
||||
|
||||
'major' => Tab::make('Major Donors')
|
||||
->icon('heroicon-o-star')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereIn('id', function ($sub) {
|
||||
$sub->select('customer_id')
|
||||
->from('donations')
|
||||
->join('donation_confirmations', 'donations.id', '=', 'donation_confirmations.donation_id')
|
||||
->whereNotNull('donation_confirmations.confirmed_at')
|
||||
->groupBy('customer_id')
|
||||
->havingRaw('SUM(donations.amount) >= 100000');
|
||||
})
|
||||
),
|
||||
|
||||
'recent' => Tab::make('New (30 days)')
|
||||
->icon('heroicon-o-sparkles')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->where('created_at', '>=', now()->subDays(30))
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
79
temp_files/v4/ListDonations.php
Normal file
79
temp_files/v4/ListDonations.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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();
|
||||
|
||||
return [
|
||||
'today' => Tab::make('Today')
|
||||
->icon('heroicon-o-clock')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereDate('created_at', today())
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
),
|
||||
|
||||
'all_confirmed' => Tab::make('All Confirmed')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
),
|
||||
|
||||
'incomplete' => Tab::make('Incomplete')
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->badge($incompleteCount > 0 ? $incompleteCount : null)
|
||||
->badgeColor('danger')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereDoesntHave('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->where('created_at', '>=', now()->subDays(7))
|
||||
),
|
||||
|
||||
'zakat' => Tab::make('Zakat')
|
||||
->icon('heroicon-o-star')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereHas('donationPreferences', fn ($q) => $q->where('is_zakat', true))
|
||||
),
|
||||
|
||||
'gift_aid' => Tab::make('Gift Aid')
|
||||
->icon('heroicon-o-gift')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query
|
||||
->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at'))
|
||||
->whereHas('donationPreferences', fn ($q) => $q->where('is_gift_aid', true))
|
||||
),
|
||||
|
||||
'everything' => Tab::make('Everything')
|
||||
->icon('heroicon-o-squares-2x2'),
|
||||
];
|
||||
}
|
||||
}
|
||||
53
temp_files/v4/ListScheduledGivingDonations.php
Normal file
53
temp_files/v4/ListScheduledGivingDonations.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ScheduledGivingDonationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ScheduledGivingDonationResource;
|
||||
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 'Regular Giving';
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$active = ScheduledGivingDonation::where('is_active', true)->count();
|
||||
return "{$active} people giving every month.";
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$active = ScheduledGivingDonation::where('is_active', true)->count();
|
||||
$inactive = ScheduledGivingDonation::where('is_active', false)->count();
|
||||
|
||||
return [
|
||||
'active' => Tab::make('Active')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->badge($active)
|
||||
->badgeColor('success')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q->where('is_active', true)),
|
||||
|
||||
'cancelled' => Tab::make('Cancelled')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->badge($inactive > 0 ? $inactive : null)
|
||||
->badgeColor('gray')
|
||||
->modifyQueryUsing(fn (Builder $q) => $q->where('is_active', false)),
|
||||
|
||||
'all' => Tab::make('All')
|
||||
->icon('heroicon-o-squares-2x2'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
183
temp_files/v4/nav_changes.py
Normal file
183
temp_files/v4/nav_changes.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reassign every Filament resource to the correct navigation group.
|
||||
|
||||
SIDEBAR STRUCTURE:
|
||||
Daily (always open, no collapse)
|
||||
├ Donors — "Find a donor, help them"
|
||||
├ Donations — "See what came in, investigate issues"
|
||||
└ Regular Giving — "Monthly supporters"
|
||||
|
||||
Fundraising
|
||||
├ Review Queue (84 pending badge)
|
||||
├ All Fundraisers
|
||||
└ Scheduled Campaigns (3 only)
|
||||
|
||||
Setup (collapsed by default — rarely touched)
|
||||
├ Causes (30)
|
||||
├ Countries (12)
|
||||
├ URL Builder
|
||||
├ Settings
|
||||
├ Users
|
||||
├ Activity Log
|
||||
└ (hidden): Snowdon, Campaign Regs, WOH, Engage dims, Fund dims
|
||||
"""
|
||||
|
||||
import re, os
|
||||
|
||||
BASE = '/home/forge/app.charityright.org.uk'
|
||||
|
||||
changes = {
|
||||
# ── DAILY (top 3 — the only pages staff use every day) ──
|
||||
'app/Filament/Resources/CustomerResource.php': {
|
||||
'navigationGroup': 'Daily',
|
||||
'navigationIcon': 'heroicon-o-user-circle',
|
||||
'navigationSort': 1,
|
||||
'navigationLabel': 'Donors',
|
||||
},
|
||||
'app/Filament/Resources/DonationResource.php': {
|
||||
'navigationGroup': 'Daily',
|
||||
'navigationIcon': 'heroicon-o-banknotes',
|
||||
'navigationSort': 2,
|
||||
'navigationLabel': 'Donations',
|
||||
},
|
||||
'app/Filament/Resources/ScheduledGivingDonationResource.php': {
|
||||
'navigationGroup': 'Daily',
|
||||
'navigationIcon': 'heroicon-o-arrow-path',
|
||||
'navigationSort': 3,
|
||||
'navigationLabel': 'Regular Giving',
|
||||
},
|
||||
|
||||
# ── FUNDRAISING ──
|
||||
'app/Filament/Resources/ApprovalQueueResource.php': {
|
||||
'navigationGroup': 'Fundraising',
|
||||
'navigationIcon': 'heroicon-o-shield-check',
|
||||
'navigationSort': 1,
|
||||
'navigationLabel': 'Review Queue',
|
||||
},
|
||||
'app/Filament/Resources/AppealResource.php': {
|
||||
'navigationGroup': 'Fundraising',
|
||||
'navigationIcon': 'heroicon-o-hand-raised',
|
||||
'navigationSort': 2,
|
||||
'navigationLabel': 'All Fundraisers',
|
||||
},
|
||||
'app/Filament/Resources/ScheduledGivingCampaignResource.php': {
|
||||
'navigationGroup': 'Fundraising',
|
||||
'navigationIcon': 'heroicon-o-calendar',
|
||||
'navigationSort': 3,
|
||||
'navigationLabel': 'Giving Campaigns',
|
||||
},
|
||||
|
||||
# ── SETUP (collapsed, rarely used) ──
|
||||
'app/Filament/Resources/DonationTypeResource.php': {
|
||||
'navigationGroup': 'Setup',
|
||||
'navigationIcon': 'heroicon-o-tag',
|
||||
'navigationSort': 1,
|
||||
'navigationLabel': 'Causes',
|
||||
},
|
||||
'app/Filament/Resources/DonationCountryResource.php': {
|
||||
'navigationGroup': 'Setup',
|
||||
'navigationIcon': 'heroicon-o-globe-alt',
|
||||
'navigationSort': 2,
|
||||
'navigationLabel': 'Countries',
|
||||
},
|
||||
'app/Filament/Resources/UserResource.php': {
|
||||
'navigationGroup': 'Setup',
|
||||
'navigationIcon': 'heroicon-o-users',
|
||||
'navigationSort': 3,
|
||||
'navigationLabel': 'Admin Users',
|
||||
},
|
||||
'app/Filament/Resources/EventLogResource.php': {
|
||||
'navigationGroup': 'Setup',
|
||||
'navigationIcon': 'heroicon-o-exclamation-triangle',
|
||||
'navigationSort': 4,
|
||||
'navigationLabel': 'Activity Log',
|
||||
},
|
||||
}
|
||||
|
||||
# Resources to HIDE from navigation entirely (dead data, 0 recent activity)
|
||||
hide = [
|
||||
'app/Filament/Resources/SnowdonRegistrationResource.php',
|
||||
'app/Filament/Resources/CampaignRegistrationResource.php',
|
||||
'app/Filament/Resources/WOHMessageResource.php',
|
||||
'app/Filament/Resources/EngageAttributionDimensionResource.php',
|
||||
'app/Filament/Resources/EngageFundDimensionResource.php',
|
||||
]
|
||||
|
||||
# Pages
|
||||
page_changes = {
|
||||
'app/Filament/Pages/DonationURLBuilder.php': {
|
||||
'navigationGroup': 'Setup',
|
||||
'navigationSort': 5,
|
||||
'navigationLabel': 'URL Builder',
|
||||
},
|
||||
'app/Filament/Pages/Settings.php': {
|
||||
'navigationGroup': 'Setup',
|
||||
'navigationSort': 6,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def update_static_property(content, prop, value):
|
||||
"""Replace a static property value in a PHP class."""
|
||||
# Match: protected static ?string $prop = 'old_value';
|
||||
# or: protected static ?int $prop = 123;
|
||||
pattern = rf"(protected\s+static\s+\?(?:string|int)\s+\${prop}\s*=\s*)('[^']*'|\d+)(\s*;)"
|
||||
|
||||
if isinstance(value, int):
|
||||
replacement = rf"\g<1>{value}\3"
|
||||
else:
|
||||
replacement = rf"\g<1>'{value}'\3"
|
||||
|
||||
new_content, count = re.subn(pattern, replacement, content)
|
||||
return new_content, count
|
||||
|
||||
|
||||
def apply_changes(filepath, props):
|
||||
full = os.path.join(BASE, filepath)
|
||||
with open(full, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
for prop, value in props.items():
|
||||
content, count = update_static_property(content, prop, value)
|
||||
if count == 0:
|
||||
print(f" WARNING: {prop} not found in {filepath}")
|
||||
|
||||
with open(full, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"Updated: {filepath}")
|
||||
|
||||
|
||||
def hide_from_nav(filepath):
|
||||
full = os.path.join(BASE, filepath)
|
||||
with open(full, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Add shouldRegisterNavigation = false if not present
|
||||
if 'shouldRegisterNavigation' not in content:
|
||||
# Insert after the class opening
|
||||
content = content.replace(
|
||||
'protected static ?string $navigationIcon',
|
||||
'protected static bool $shouldRegisterNavigation = false;\n\n protected static ?string $navigationIcon'
|
||||
)
|
||||
with open(full, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"Hidden: {filepath}")
|
||||
else:
|
||||
print(f"Already hidden: {filepath}")
|
||||
|
||||
|
||||
# Apply all changes
|
||||
print("=== UPDATING NAVIGATION ===")
|
||||
for filepath, props in changes.items():
|
||||
apply_changes(filepath, props)
|
||||
|
||||
print("\n=== HIDING DEAD PAGES ===")
|
||||
for filepath in hide:
|
||||
hide_from_nav(filepath)
|
||||
|
||||
print("\n=== UPDATING PAGES ===")
|
||||
for filepath, props in page_changes.items():
|
||||
apply_changes(filepath, props)
|
||||
|
||||
print("\nDone!")
|
||||
Reference in New Issue
Block a user