<?php
declare(strict_types=1);
namespace App\Controller\Backend;
use App\Contracts\Platform\Platform;
use App\Entity\Activity;
use App\Entity\Administrator;
use App\Entity\Badge;
use App\Entity\Campaign;
use App\Entity\Complaint;
use App\Entity\ComplaintReason;
use App\Entity\Country;
use App\Entity\Cpm;
use App\Entity\Creator;
use App\Entity\Manager;
use App\Entity\PaymentHistory;
use App\Entity\PaymentProcess;
use App\Entity\ScrapingCreatorsData;
use App\Entity\ScrapingVideosData;
use App\Entity\Settings;
use App\Entity\Speed;
use App\Entity\Sponsor;
use App\Entity\User;
use App\Services\Statistic\GlobalStatisticService;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\User\UserInterface;
class DashboardController extends AbstractDashboardController
{
public function __construct(
private readonly GlobalStatisticService $statistics,
) {
}
#[Route(
path: '/',
name: 'backend-dashboard',
)]
public function index(): Response
{
// Shell only: the overview is fetched async (backend-dashboard-overview), so
// landing here — straight after login — never blocks on the stats aggregates.
return $this->render('backend/dashboard.html.twig');
}
#[Route(
path: '/dashboard/overview',
name: 'backend-dashboard-overview',
)]
public function overviewFragment(): Response
{
// "Today" vs the same window yesterday (trend), plus all-time totals + QC backlog.
$now = new \DateTimeImmutable();
$today = $now->setTime(0, 0);
$overview = $this->statistics->dashboardOverview(
$today,
$now,
$today->modify('-1 day'),
$now->modify('-1 day'),
'today',
);
return $this->render('backend/_dashboard_overview.html.twig', ['overview' => $overview]);
}
#[Route(
path: '/statistic',
name: 'statistic-page',
)]
public function statistic(Request $request): Response
{
[$from, $to, $period, $prevFrom, $prevTo] = $this->resolvePeriod($request);
return $this->render('backend/statistic/statistic.html.twig', [
'overview' => $this->statistics->overview($from, $to, $prevFrom, $prevTo, $period === 'custom' ? null : $period),
'period' => $period,
'periods' => [7, 14, 28, 60, 90, 120, 365],
'from' => $from?->format('Y-m-d'),
'to' => $to?->format('Y-m-d'),
]);
}
#[Route(path: '/statistic/data', name: 'statistic-data')]
public function statisticData(Request $request): Response
{
[$from, $to, $period, $prevFrom, $prevTo] = $this->resolvePeriod($request);
return new JsonResponse([
'period' => $period,
'from' => $from?->format('Y-m-d'),
'to' => $to?->format('Y-m-d'),
'overview' => $this->statistics->overview($from, $to, $prevFrom, $prevTo, $period === 'custom' ? null : $period),
]);
}
/** @return array{0: ?\DateTimeImmutable, 1: ?\DateTimeImmutable, 2: string} */
private function resolvePeriod(Request $request): array
{
$period = (string) $request->query->get('period', 'all');
$now = new \DateTimeImmutable();
$from = null;
$to = null;
if ($period === 'custom') {
$f = $request->query->get('from');
$t = $request->query->get('to');
$from = $f ? new \DateTimeImmutable($f . ' 00:00:00') : null;
$to = $t ? new \DateTimeImmutable($t . ' 23:59:59') : null;
} elseif (ctype_digit($period)) {
$from = $now->modify('-' . $period . ' days');
$to = $now;
} // 'all' => open-ended
// Previous equal-length window immediately before [from, to], for trend.
$prevFrom = null;
$prevTo = null;
if ($from !== null && $to !== null) {
$seconds = $to->getTimestamp() - $from->getTimestamp();
$prevFrom = $from->modify('-' . $seconds . ' seconds');
$prevTo = $from;
}
return [$from, $to, $period, $prevFrom, $prevTo];
}
#[Route(
path: '/statistic-global',
name: 'statistic-global',
)]
public function globalStats(
GlobalStatisticService $globalStatisticService,
): Response {
// Served from cache (the aggregates take ~20s; the scheduler keeps it
// warm via app:statistic:global:warm). Shape matches back_office_stats.js.
return new JsonResponse($globalStatisticService->getCached());
}
public function configureUserMenu(UserInterface $user): UserMenu
{
/** @var User $user */
return parent::configureUserMenu($user)
->setName($user->getFullname());
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('<span style="font-weight:800;font-size:1.15rem;letter-spacing:-.02em;">Sound<span style="color:#635bff;">.ME</span></span>')
->setTranslationDomain('messages')
->renderContentMaximized();
}
public function configureAssets(): Assets
{
// Global list/datagrid styling for all CRUD tabs. Injected as a raw <link>
// (not addCssFile) to bypass the asset() manifest pipeline (no webpack build locally).
return parent::configureAssets()
->addHtmlContentToHead('<link rel="stylesheet" href="/assets/css/sm-admin.css">');
}
public function configureMenuItems(): iterable
{
return [
MenuItem::linkToDashboard('Dashboard', 'fa fa-home'),
MenuItem::linkToRoute('Statistics', 'fas fa-chart-bar', 'statistic-page'),
MenuItem::section('Campaigns'),
MenuItem::linkToCrud('Campaigns', 'fa fa-file-invoice-dollar', Campaign::class),
MenuItem::linkToCrud('Activities', 'fa fa-chart-line', Activity::class),
MenuItem::subMenu('Complaints', 'fa fa-commenting')->setSubItems([
MenuItem::linkToCrud('Complaints', 'fa fa-commenting', Complaint::class),
MenuItem::linkToCrud('Complaint Reasons', 'fas fa-info-circle', ComplaintReason::class),
]),
MenuItem::section('Creators'),
MenuItem::linkToCrud('All Creators', 'fas fa-users', Creator::class)
->setController(CreatorCrudController::class),
MenuItem::linkToCrud('VIP Creators', 'fas fa-star', Creator::class)
->setController(VipCreatorCrudController::class),
MenuItem::linkToRoute('Communication', 'fas fa-comments', 'backend-communication-creator'),
MenuItem::subMenu('Scraping', 'fa fa-file')->setSubItems([
MenuItem::linkToRoute('Scrape Creators', 'fa fa-file', 'backend-scrape-page'),
MenuItem::linkToCrud('Scraped Creators', 'fa fa-list', Creator::class)
->setController(CreatorScrapingCrudController::class),
MenuItem::linkToCrud('Creators Data', 'fa fa-list', ScrapingCreatorsData::class),
MenuItem::linkToCrud('Videos Data', 'fa fa-video', ScrapingVideosData::class),
]),
MenuItem::section('Sponsors & Billing'),
MenuItem::linkToCrud('Sponsors', 'fas fa-dollar-sign', Sponsor::class),
MenuItem::linkToRoute('Communication', 'fas fa-comments', 'backend-communication-sponsor'),
MenuItem::linkToCrud('Payments', 'fa fa-money-check-alt', PaymentHistory::class),
MenuItem::linkToCrud('Payment Process', 'fa fa-list-check', PaymentProcess::class),
MenuItem::section('Team'),
MenuItem::linkToCrud('Managers', 'fas fa-people-roof', Manager::class),
MenuItem::linkToCrud('Administrators', 'fas fa-user-shield', Administrator::class),
MenuItem::section('System'),
MenuItem::linkToCrud('Badges', 'fas fa-certificate', Badge::class),
MenuItem::linkToCrud('Countries', 'fas fa-globe-americas', Country::class),
MenuItem::linkToCrud('CPM', 'fa fa-jpy', Cpm::class),
MenuItem::linkToCrud('Speed', 'fas fa-tachometer-alt', Speed::class),
MenuItem::linkToCrud('Settings', 'fas fa-tools', Settings::class)
->setAction(Action::NEW),
];
}
}