Fix dead space: remove max-w-6xl, edge-to-edge heroes, full-width layout
Layout: - Removed max-w-6xl from <main> — content now fills available width - Removed padding from <main> — 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)
This commit is contained in:
@@ -152,7 +152,7 @@ export default function AutomationsPage() {
|
||||
const overallRate = totalSentAll > 0 ? Math.round((totalConverted / totalSentAll) * 100) : 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
{/* ━━ HERO — Full-width, same pattern as landing page ━━━━━━━ */}
|
||||
<div className="grid md:grid-cols-5 gap-0">
|
||||
@@ -182,6 +182,9 @@ export default function AutomationsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content with padding ── */}
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-6">
|
||||
|
||||
{/* ━━ TWO-COLUMN LAYOUT — Phone left, education right ━━━━━━ */}
|
||||
<div className="grid lg:grid-cols-12 gap-6">
|
||||
|
||||
@@ -505,6 +508,8 @@ export default function AutomationsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function CollectPage() {
|
||||
// ── No events yet ──
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-[#111827] tracking-tight">Collect</h1>
|
||||
<p className="text-sm text-gray-500 mt-0.5">Create an appeal and share pledge links</p>
|
||||
@@ -219,10 +219,10 @@ export default function CollectPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
{/* ━━ HERO — Brand photography + context ━━━━━━━━━━━━━━━━━━━ */}
|
||||
<div className="grid md:grid-cols-5 gap-0 mb-6">
|
||||
<div className="grid md:grid-cols-5 gap-0">
|
||||
<div className="md:col-span-2 relative min-h-[140px] md:min-h-[180px] overflow-hidden">
|
||||
<Image
|
||||
src="/images/brand/event-05-qr-scanning.jpg"
|
||||
@@ -244,6 +244,9 @@ export default function CollectPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content with padding ── */}
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-6">
|
||||
|
||||
{/* ── Appeals as visible cards (not hidden in a dropdown) ── */}
|
||||
{events.length > 1 && (
|
||||
<div className="space-y-2">
|
||||
@@ -511,6 +514,8 @@ export default function CollectPage() {
|
||||
creating={creatingAppeal} onCreate={createAppeal}
|
||||
onCancel={() => setShowNewAppeal(false)}
|
||||
/>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function ReportsPage() {
|
||||
const giftAidReclaimable = Math.round(giftAidTotal * 0.25)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
|
||||
{/* ━━ HERO — Financial summary as a landing page section ━━━ */}
|
||||
<div className="grid md:grid-cols-5 gap-0">
|
||||
@@ -106,6 +106,9 @@ export default function ReportsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content with padding ── */}
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-8">
|
||||
|
||||
{/* ── Financial breakdown ── */}
|
||||
<div className="bg-[#111827] p-6">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-gray-700">
|
||||
@@ -384,6 +387,8 @@ export default function ReportsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
</nav>
|
||||
|
||||
{/* Main content — white background, generous padding */}
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 md:pb-8 max-w-6xl">
|
||||
<main className="flex-1 pb-20 md:pb-8 overflow-hidden">
|
||||
<WhatsAppBanner />
|
||||
{children}
|
||||
</main>
|
||||
@@ -182,7 +182,7 @@ function WhatsAppBanner() {
|
||||
if (status === "CONNECTED" || status === "skip" || status === null || dismissed) return null
|
||||
|
||||
return (
|
||||
<div className="mb-6 border-l-2 border-[#F59E0B] bg-[#F59E0B]/5 p-4 flex items-start gap-3">
|
||||
<div className="mx-4 md:mx-6 lg:mx-8 mt-4 md:mt-6 lg:mt-8 border-l-2 border-[#F59E0B] bg-[#F59E0B]/5 p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-[#F59E0B] shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-bold text-[#111827]">WhatsApp not connected — reminders won't send</p>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function DashboardPage() {
|
||||
: "Friends at a charity dinner — where pledges begin"
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
{/* ━━ HERO — Brand photography + the one thing that matters ━━━ */}
|
||||
<div className="grid md:grid-cols-5 gap-0">
|
||||
@@ -202,6 +202,9 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content with padding ── */}
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-6">
|
||||
|
||||
{/* ── Empty state: Share your link ── */}
|
||||
{isEmpty && pledgeLink && (
|
||||
<div className="space-y-6">
|
||||
@@ -487,6 +490,8 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,10 +258,10 @@ export default function MoneyPage() {
|
||||
if (loading) return <div className="flex items-center justify-center py-20"><Loader2 className="h-6 w-6 text-[#1E40AF] animate-spin" /></div>
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
{/* ━━ HERO — Dark stats panel like landing page ━━━━━━━━━━━━ */}
|
||||
<div className="grid md:grid-cols-5 gap-0 mb-6">
|
||||
<div className="grid md:grid-cols-5 gap-0">
|
||||
<div className="md:col-span-2 relative min-h-[140px] md:min-h-[180px] overflow-hidden">
|
||||
<Image
|
||||
src="/images/brand/ops-06-counting-money.jpg"
|
||||
@@ -284,6 +284,9 @@ export default function MoneyPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content with padding ── */}
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-6">
|
||||
|
||||
{/* ── Stats bar (clickable filters) ── */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-px bg-gray-200">
|
||||
{[
|
||||
@@ -789,6 +792,8 @@ export default function MoneyPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,10 +186,10 @@ export default function SettingsPage() {
|
||||
: `${totalCount - doneCount} thing${totalCount - doneCount > 1 ? "s" : ""} left before you go live.`
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
{/* ── Header — human progress, not a form page ── */}
|
||||
<div className={`p-6 mb-6 ${doneCount === totalCount ? "bg-[#16A34A]" : "bg-[#111827]"}`}>
|
||||
<div className={`p-6 ${doneCount === totalCount ? "bg-[#16A34A]" : "bg-[#111827]"}`}>
|
||||
<div className="border-l-2 border-[#F59E0B] pl-3 mb-3">
|
||||
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Settings</p>
|
||||
</div>
|
||||
@@ -208,6 +208,9 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content with padding ── */}
|
||||
<div className="p-4 md:p-6 lg:p-8 space-y-6">
|
||||
|
||||
{error && <div className="border-l-2 border-[#DC2626] bg-[#DC2626]/5 p-3 text-sm text-[#DC2626]">{error}</div>}
|
||||
|
||||
{/* ━━ TWO-COLUMN: Checklist left, Education right ━━━━━━ */}
|
||||
@@ -584,6 +587,8 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
95
temp_files/fix/ListDonations.php
Normal file
95
temp_files/fix/ListDonations.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?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();
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
204
temp_files/fix/ScheduledGivingDashboard.php
Normal file
204
temp_files/fix/ScheduledGivingDashboard.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\ScheduledGivingCampaign;
|
||||
use App\Models\ScheduledGivingDonation;
|
||||
use App\Models\ScheduledGivingPayment;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Scheduled Giving Command Centre.
|
||||
*
|
||||
* Three campaigns, three seasons, one dashboard.
|
||||
* Every number a supporter-care agent needs is on this page.
|
||||
*/
|
||||
class ScheduledGivingDashboard extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'heroicon-o-calendar-days';
|
||||
|
||||
protected static ?string $navigationGroup = 'Seasonal Campaigns';
|
||||
|
||||
protected static ?int $navigationSort = 0;
|
||||
|
||||
protected static ?string $navigationLabel = 'Campaign Dashboard';
|
||||
|
||||
protected static ?string $title = 'Scheduled Giving';
|
||||
|
||||
protected static string $view = 'filament.pages.scheduled-giving-dashboard';
|
||||
|
||||
/**
|
||||
* Aggregate stats for every campaign, plus global totals.
|
||||
* Runs 3 efficient queries (not N+1).
|
||||
*/
|
||||
public function getCampaignData(): array
|
||||
{
|
||||
$campaigns = ScheduledGivingCampaign::withCount([
|
||||
'donations',
|
||||
'donations as active_count' => 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user