From dc8e593849a5f291736e8b8b5bd251868b9ea89b Mon Sep 17 00:00:00 2001 From: Omair Saleh Date: Thu, 5 Mar 2026 03:49:02 +0800 Subject: [PATCH] Fix dead space: remove max-w-6xl, edge-to-edge heroes, full-width layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout: - Removed max-w-6xl from
— content now fills available width - Removed padding from
— each page manages its own padding - Heroes go edge-to-edge (no inner margins) - Content below heroes has p-4 md:p-6 lg:p-8 padding wrapper - WhatsApp banner has its own margin so it doesn't break hero bleed - overflow-hidden on main prevents horizontal scroll from heroes All 6 pages: - Hero section sits flush against edges (no gaps) - Content below hero wrapped in padding container - Two-column grids now use the FULL available width - On a 1920px screen: sidebar 192px + content fills remaining ~1728px - Right columns are now substantial (5/12 of full width = ~720px) --- .../src/app/dashboard/automations/page.tsx | 7 +- .../src/app/dashboard/collect/page.tsx | 11 +- .../src/app/dashboard/exports/page.tsx | 7 +- .../src/app/dashboard/layout.tsx | 4 +- .../src/app/dashboard/page.tsx | 7 +- .../src/app/dashboard/pledges/page.tsx | 9 +- .../src/app/dashboard/settings/page.tsx | 9 +- temp_files/fix/ListDonations.php | 95 ++++++++ temp_files/fix/ScheduledGivingDashboard.php | 204 ++++++++++++++++++ 9 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 temp_files/fix/ListDonations.php create mode 100644 temp_files/fix/ScheduledGivingDashboard.php diff --git a/pledge-now-pay-later/src/app/dashboard/automations/page.tsx b/pledge-now-pay-later/src/app/dashboard/automations/page.tsx index d41a6ec..23a84da 100644 --- a/pledge-now-pay-later/src/app/dashboard/automations/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/automations/page.tsx @@ -152,7 +152,7 @@ export default function AutomationsPage() { const overallRate = totalSentAll > 0 ? Math.round((totalConverted / totalSentAll) * 100) : 0 return ( -
+
{/* ━━ HERO — Full-width, same pattern as landing page ━━━━━━━ */}
@@ -182,6 +182,9 @@ export default function AutomationsPage() {
+ {/* ── Content with padding ── */} +
+ {/* ━━ TWO-COLUMN LAYOUT — Phone left, education right ━━━━━━ */}
@@ -505,6 +508,8 @@ export default function AutomationsPage() {
+ + ) } diff --git a/pledge-now-pay-later/src/app/dashboard/collect/page.tsx b/pledge-now-pay-later/src/app/dashboard/collect/page.tsx index e865135..e296833 100644 --- a/pledge-now-pay-later/src/app/dashboard/collect/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/collect/page.tsx @@ -186,7 +186,7 @@ export default function CollectPage() { // ── No events yet ── if (events.length === 0) { return ( -
+

Collect

Create an appeal and share pledge links

@@ -219,10 +219,10 @@ export default function CollectPage() { } return ( -
+
{/* ━━ HERO — Brand photography + context ━━━━━━━━━━━━━━━━━━━ */} -
+
+ {/* ── Content with padding ── */} +
+ {/* ── Appeals as visible cards (not hidden in a dropdown) ── */} {events.length > 1 && (
@@ -511,6 +514,8 @@ export default function CollectPage() { creating={creatingAppeal} onCreate={createAppeal} onCancel={() => setShowNewAppeal(false)} />} + +
) } diff --git a/pledge-now-pay-later/src/app/dashboard/exports/page.tsx b/pledge-now-pay-later/src/app/dashboard/exports/page.tsx index 0a0ef36..5ad8bcc 100644 --- a/pledge-now-pay-later/src/app/dashboard/exports/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/exports/page.tsx @@ -77,7 +77,7 @@ export default function ReportsPage() { const giftAidReclaimable = Math.round(giftAidTotal * 0.25) return ( -
+
{/* ━━ HERO — Financial summary as a landing page section ━━━ */}
@@ -106,6 +106,9 @@ export default function ReportsPage() {
+ {/* ── Content with padding ── */} +
+ {/* ── Financial breakdown ── */}
@@ -384,6 +387,8 @@ export default function ReportsPage() { )}
+ +
) } diff --git a/pledge-now-pay-later/src/app/dashboard/layout.tsx b/pledge-now-pay-later/src/app/dashboard/layout.tsx index 700da6a..6b906dd 100644 --- a/pledge-now-pay-later/src/app/dashboard/layout.tsx +++ b/pledge-now-pay-later/src/app/dashboard/layout.tsx @@ -156,7 +156,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod {/* Main content — white background, generous padding */} -
+
{children}
@@ -182,7 +182,7 @@ function WhatsAppBanner() { if (status === "CONNECTED" || status === "skip" || status === null || dismissed) return null return ( -
+

WhatsApp not connected — reminders won't send

diff --git a/pledge-now-pay-later/src/app/dashboard/page.tsx b/pledge-now-pay-later/src/app/dashboard/page.tsx index a601537..665a527 100644 --- a/pledge-now-pay-later/src/app/dashboard/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/page.tsx @@ -134,7 +134,7 @@ export default function DashboardPage() { : "Friends at a charity dinner — where pledges begin" return ( -
+
{/* ━━ HERO — Brand photography + the one thing that matters ━━━ */}
@@ -202,6 +202,9 @@ export default function DashboardPage() {
+ {/* ── Content with padding ── */} +
+ {/* ── Empty state: Share your link ── */} {isEmpty && pledgeLink && (
@@ -487,6 +490,8 @@ export default function DashboardPage() {
)} + +
) } diff --git a/pledge-now-pay-later/src/app/dashboard/pledges/page.tsx b/pledge-now-pay-later/src/app/dashboard/pledges/page.tsx index bb52ace..f483619 100644 --- a/pledge-now-pay-later/src/app/dashboard/pledges/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/pledges/page.tsx @@ -258,10 +258,10 @@ export default function MoneyPage() { if (loading) return
return ( -
+
{/* ━━ HERO — Dark stats panel like landing page ━━━━━━━━━━━━ */} -
+
+ {/* ── Content with padding ── */} +
+ {/* ── Stats bar (clickable filters) ── */}
{[ @@ -789,6 +792,8 @@ export default function MoneyPage() {
+ +
) } 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 c073ec9..54c15b3 100644 --- a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx @@ -186,10 +186,10 @@ export default function SettingsPage() { : `${totalCount - doneCount} thing${totalCount - doneCount > 1 ? "s" : ""} left before you go live.` return ( -
+
{/* ── Header — human progress, not a form page ── */} -
+

Settings

@@ -208,6 +208,9 @@ export default function SettingsPage() {
+ {/* ── Content with padding ── */} +
+ {error &&
{error}
} {/* ━━ TWO-COLUMN: Checklist left, Education right ━━━━━━ */} @@ -584,6 +587,8 @@ export default function SettingsPage() {
+ +
) } diff --git a/temp_files/fix/ListDonations.php b/temp_files/fix/ListDonations.php new file mode 100644 index 0000000..5264341 --- /dev/null +++ b/temp_files/fix/ListDonations.php @@ -0,0 +1,95 @@ + $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(); + + 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)) + ), + + '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') + ->icon('heroicon-o-squares-2x2'), + ]; + } + + public function getDefaultActiveTab(): string | int | null + { + return 'today'; + } +} diff --git a/temp_files/fix/ScheduledGivingDashboard.php b/temp_files/fix/ScheduledGivingDashboard.php new file mode 100644 index 0000000..be60504 --- /dev/null +++ b/temp_files/fix/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(); + } +}