- Changed super admin detection from role-based to `is_super_admin` flag in AdminUser model
- Added `requireDefaultAdminSiteId()` method to throw validation error when no integration site exists
- Enhanced site deletion to migrate platform role bindings to fallback site and auto-delete site-specific admin accounts
- Made agent line code optional with auto-generation fallback using `{site_code}-agent-{counter}` format
138 lines
5.0 KiB
PHP
138 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin;
|
|
|
|
use App\Models\AdminSite;
|
|
use App\Models\AdminUser;
|
|
use App\Models\AgentNode;
|
|
use App\Models\Player;
|
|
use App\Support\AdminScopeContext;
|
|
use App\Support\AdminScopeContextResolver;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/** 站点管理员仪表盘:站点规模、今日/近 7 日经营、待结账单摘要。 */
|
|
final class SiteDashboardOverviewBuilder
|
|
{
|
|
public function __construct(
|
|
private readonly AdminReportQueryService $reportQuery,
|
|
) {}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function build(AdminUser $admin, AdminScopeContext $scope): ?array
|
|
{
|
|
if (! $admin->hasPermissionCode('dashboard.view')) {
|
|
return null;
|
|
}
|
|
|
|
$site = $this->resolvePrimarySite($admin);
|
|
if ($site === null) {
|
|
return null;
|
|
}
|
|
|
|
$siteId = (int) $site->id;
|
|
$siteCode = (string) $site->code;
|
|
$agentNodeIds = AgentNode::query()
|
|
->where('admin_site_id', $siteId)
|
|
->pluck('id')
|
|
->map(static fn ($id): int => (int) $id)
|
|
->all();
|
|
|
|
$playerCount = (int) Player::query()->where('site_code', $siteCode)->count();
|
|
$today = now()->toDateString();
|
|
$sevenDayFrom = now()->subDays(6)->toDateString();
|
|
$scoped = AdminScopeContextResolver::fromValues(
|
|
$admin,
|
|
requestedSiteCode: $siteCode,
|
|
);
|
|
$todayTotals = $this->reportQuery->periodFinanceTotals($today, $today, $scoped);
|
|
$sevenDayTotals = $this->reportQuery->periodFinanceTotals($sevenDayFrom, $today, $scoped);
|
|
$currencyCode = $this->reportQuery->resolvePeriodCurrencyCode($today, $today, $scoped)
|
|
?? $this->reportQuery->resolvePeriodCurrencyCode($sevenDayFrom, $today, $scoped);
|
|
$todayActivity = $this->todayActivityStats($siteCode, $today);
|
|
$pendingBills = $this->pendingBillStats($siteId);
|
|
$topAgentToday = $this->topAgentToday($scoped, $today);
|
|
|
|
return [
|
|
'admin_site_id' => $siteId,
|
|
'site_code' => $siteCode,
|
|
'site_name' => (string) $site->name,
|
|
'agent_count' => count($agentNodeIds),
|
|
'player_count' => $playerCount,
|
|
'active_player_count_today' => $todayActivity['player_count'],
|
|
'bet_order_count_today' => $todayActivity['order_count'],
|
|
'today_bet_minor' => $todayTotals['total_bet_minor'],
|
|
'today_payout_minor' => $todayTotals['total_payout_minor'],
|
|
'today_profit_minor' => $todayTotals['approx_house_gross_minor'],
|
|
'seven_day_bet_minor' => $sevenDayTotals['total_bet_minor'],
|
|
'seven_day_payout_minor' => $sevenDayTotals['total_payout_minor'],
|
|
'seven_day_profit_minor' => $sevenDayTotals['approx_house_gross_minor'],
|
|
'profit_scope' => 'house_gross',
|
|
'currency_code' => $currencyCode,
|
|
'pending_bill_count' => $pendingBills['count'],
|
|
'pending_unpaid_minor' => $pendingBills['unpaid_minor'],
|
|
'latest_bet_at' => $todayActivity['latest_bet_at'],
|
|
'top_agent_today' => $topAgentToday,
|
|
];
|
|
}
|
|
|
|
private function resolvePrimarySite(AdminUser $admin): ?AdminSite
|
|
{
|
|
$siteIds = $admin->accessibleAdminSiteIds();
|
|
if ($siteIds === null || $siteIds === []) {
|
|
return null;
|
|
}
|
|
|
|
return AdminSite::query()
|
|
->where('id', (int) $siteIds[0])
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* @return array{player_count: int, order_count: int, latest_bet_at: ?string}
|
|
*/
|
|
private function todayActivityStats(string $siteCode, string $today): array
|
|
{
|
|
$base = DB::table('ticket_orders as o')
|
|
->join('players as p', 'p.id', '=', 'o.player_id')
|
|
->where('p.site_code', $siteCode)
|
|
->whereDate('o.created_at', $today);
|
|
|
|
$latestBetAt = (clone $base)->max('o.created_at');
|
|
|
|
return [
|
|
'player_count' => (int) (clone $base)->distinct('o.player_id')->count('o.player_id'),
|
|
'order_count' => (int) (clone $base)->count(),
|
|
'latest_bet_at' => $latestBetAt !== null ? Carbon::parse((string) $latestBetAt)->toIso8601String() : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{count: int, unpaid_minor: int}
|
|
*/
|
|
private function pendingBillStats(int $siteId): array
|
|
{
|
|
$query = DB::table('settlement_bills as sb')
|
|
->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
|
->where('sp.admin_site_id', $siteId)
|
|
->whereIn('sb.status', ['pending', 'pending_confirm', 'partial']);
|
|
|
|
return [
|
|
'count' => (int) $query->count(),
|
|
'unpaid_minor' => (int) $query->sum('sb.unpaid_amount'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function topAgentToday(AdminScopeContext $scope, string $today): ?array
|
|
{
|
|
$rows = $this->reportQuery->agentRankingRows($today, $today, null, 1, $scope);
|
|
|
|
return $rows[0] ?? null;
|
|
}
|
|
}
|