1ecb965981
6-я плитка «👥 Клиенты» со светофором (amber если есть спящие) + drill: KPI за период (всего активных / новых / заходили / получали лиды / платили), список новых клиентов (с датой входа/лидами/балансом) и «спящих» (активные без входа 14+ дней или ни разу = не активировались). Клик по строке → карточка клиента. Backend: clients() endpoint + clientsTile в summary (cross-tenant через pgsql_admin); сигналы — users.last_login_at, deals, balance_transactions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
3.0 KiB
PHP
62 lines
3.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
DB::table('balance_transactions')->delete();
|
|
DB::table('users')->delete();
|
|
DB::table('tenants')->delete();
|
|
});
|
|
|
|
function seedTenant(array $over = []): int
|
|
{
|
|
return DB::table('tenants')->insertGetId(array_merge([
|
|
'subdomain' => 'acme'.uniqid(), 'organization_name' => 'Acme', 'contact_email' => 'a@acme.ru',
|
|
'status' => 'active', 'is_trial' => false, 'balance_rub' => 100, 'balance_leads' => 0,
|
|
'chargeback_unrecovered_rub' => 0, 'delivered_in_month' => 0,
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
], $over));
|
|
}
|
|
|
|
it('GET /api/admin/dashboard/clients возвращает KPI + новых + спящих', function () {
|
|
// новый клиент, заходил недавно
|
|
$t1 = seedTenant(['organization_name' => 'Новый Актив', 'created_at' => now()->subDays(2)]);
|
|
DB::table('users')->insert(['tenant_id' => $t1, 'email' => 'u1@x.ru',
|
|
'password_hash' => bcrypt('x'), 'last_login_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now()]);
|
|
// новый клиент, НИ РАЗУ не заходил → спящий
|
|
$t2 = seedTenant(['organization_name' => 'Не Активировался', 'created_at' => now()->subDays(3)]);
|
|
DB::table('users')->insert(['tenant_id' => $t2, 'email' => 'u2@x.ru',
|
|
'password_hash' => bcrypt('x'), 'last_login_at' => null, 'created_at' => now(), 'updated_at' => now()]);
|
|
|
|
$res = $this->getJson('/api/admin/dashboard/clients?period=30d');
|
|
|
|
$res->assertOk();
|
|
$res->assertJsonStructure([
|
|
'kpi' => ['total_active', 'new_count', 'logged_in', 'got_leads', 'paid'],
|
|
'new_clients' => [['id', 'organization_name', 'created_at', 'last_login_at', 'delivered_in_month', 'balance_rub', 'status']],
|
|
'dormant' => [['id', 'organization_name', 'last_login_at', 'balance_rub']],
|
|
]);
|
|
expect($res->json('kpi.total_active'))->toBe(2);
|
|
expect($res->json('kpi.new_count'))->toBe(2);
|
|
expect($res->json('kpi.logged_in'))->toBe(1);
|
|
// спящий = не активировавшийся t2
|
|
expect(collect($res->json('dormant'))->pluck('organization_name'))->toContain('Не Активировался');
|
|
});
|
|
|
|
it('summary включает плитку clients со светофором', function () {
|
|
$t = seedTenant(['created_at' => now()->subDays(1)]);
|
|
DB::table('users')->insert(['tenant_id' => $t, 'email' => 'u@x.ru',
|
|
'password_hash' => bcrypt('x'), 'last_login_at' => null, 'created_at' => now(), 'updated_at' => now()]);
|
|
|
|
$res = $this->getJson('/api/admin/dashboard?period=30d');
|
|
$res->assertOk();
|
|
$res->assertJsonStructure(['clients' => ['light', 'total_active', 'new_count', 'logged_in', 'dormant']]);
|
|
// есть не активировавшийся → светофор amber
|
|
expect($res->json('clients.light'))->toBe('amber');
|
|
});
|