authorize('customers.manage'); $customer = Customer::query()->where('uuid', $uuid)->first(); if ($customer === null) { return; } // A closed account is terminal — the suspend/reactivate toggle must not // resurrect it to active while closed_at is still set. if ($customer->closed_at !== null || $customer->status === 'closed') { return; } $customer->update([ 'status' => $customer->status === 'suspended' ? 'active' : 'suspended', ]); $this->dispatch('notify', message: __('admin.customer_'.($customer->status === 'suspended' ? 'suspended' : 'reactivated'))); } public function render() { $locale = app()->getLocale(); $plans = app(PlanCatalogue::class)->sellable(); $customers = Customer::query() ->with(['instances.subscription']) ->orderBy('name') ->get(); $rows = $customers->map(function (Customer $c) use ($plans, $locale) { $instance = $c->instances->sortByDesc('id')->first(); $planKey = $instance->plan ?? null; // What this customer actually pays, off their contract — not what // the plan costs today. Otherwise a price rise inflates reported // revenue for every grandfathered customer overnight. // // Per MONTH, whatever the term: a yearly contract stores the whole // year, and adding that to a monthly column would report twelve // times the revenue for anyone who paid up front. $contract = $instance?->subscription; $priceCents = (int) ($contract?->monthlyPriceCents() ?? ($planKey !== null ? ($plans[$planKey]['price_cents'] ?? 0) : 0)); return [ 'uuid' => $c->uuid, 'name' => $c->name, 'plan' => $planKey !== null ? __('billing.plan.'.$planKey) : '—', // A gift or a discount, not a sale — flagged here rather than // inferred from the price, which a genuinely cheap plan could // also show. 'granted' => (bool) $contract?->isGranted(), 'mrr' => Number::currency($priceCents / 100, in: 'EUR', locale: $locale), 'instance' => $instance->subdomain ?? '—', // Only an instance that exists can hand out an admin login. 'instance_uuid' => ($instance?->status === 'active') ? $instance->uuid : null, 'closed' => $c->closed_at !== null || $c->status === 'closed', 'suspended' => $c->status === 'suspended', 'status' => match (true) { $c->closed_at !== null || $c->status === 'closed' => 'closed', $c->status === 'suspended' => 'suspended', default => $instance->status ?? $c->status ?? 'provisioning', }, ]; })->all(); // Plan distribution for the doughnut — derived from live instances, and // keyed by what customers are actually ON, not by what is on sale. A // withdrawn plan still has customers, and dropping them from the chart // would quietly understate the estate. $labels = []; $counts = []; foreach (PlanFamily::query()->orderBy('tier')->pluck('key') as $planKey) { $n = $customers->filter(fn (Customer $c) => $c->instances->contains('plan', $planKey))->count(); if ($n > 0) { $labels[] = __('billing.plan.'.$planKey); $counts[] = $n; } } return view('livewire.admin.customers', [ 'rows' => $rows, 'plansChart' => [ 'type' => 'doughnut', 'data' => [ 'labels' => $labels, 'datasets' => [[ 'data' => $counts, 'backgroundColor' => ['token:info', 'token:accent', 'token:success-bright', 'token:warning'], 'borderWidth' => 0, ]], ], 'options' => [ 'cutout' => '62%', 'plugins' => ['legend' => ['position' => 'bottom', 'labels' => ['boxWidth' => 10, 'padding' => 12]]], ], ], ]); } }