src/CompanyGroupBundle/Controller/OwnerDashboardController.php line 26

Open in your IDE?
  1. <?php
  2. namespace CompanyGroupBundle\Controller;
  3. use ApplicationBundle\Interfaces\SessionCheckInterface;
  4. use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
  5. use Symfony\Component\HttpFoundation\JsonResponse;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. /**
  9.  * OwnerDashboardController
  10.  *
  11.  * Company Owner Dashboard — central-server view for a business owner managing
  12.  * multiple companies. All routes are under /my/.
  13.  *
  14.  * Auth guard: every action calls requireOwnerSession() which redirects to
  15.  * 'dashboard' (the ERP login) if the user is not logged in on the central server.
  16.  */
  17. class OwnerDashboardController extends CompanyGroupGenericController implements SessionCheckInterface
  18. {
  19.     // =========================================================================
  20.     // MAIN DASHBOARD
  21.     // =========================================================================
  22.     public function dashboardAction(Request $request)
  23.     {
  24.         if (!$this->requireOwnerSession($request)) {
  25.             return $this->redirectToRoute('dashboard');
  26.         }
  27.         $userId = (int)$this->loggedUserId($request);
  28.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  29.         $svc $this->get('app.owner_dashboard_service');
  30.         $companies     $svc->getOwnedCompanies($userId);
  31.         $appIds        array_column($companies'appId');
  32.         $subscriptions $svc->getSubscriptionsForCompanies(array_map('intval'$appIds));
  33.         $chartData     $svc->getRevenueChartData(array_map('intval'$appIds), 6);
  34.         // Load ERP snapshots from cache (stale is fine for initial page load —
  35.         // the page JS will trigger per-company refresh for the live KPI cards).
  36.         $snapshots $svc->fetchAllErpSnapshots($companiesfalse);
  37.         $aggregate $svc->aggregateFinancials($companies$snapshots);
  38.         $kpis      $svc->aggregateKpis($snapshots);
  39.         $alerts    $svc->buildAlerts($companies$snapshots$subscriptions);
  40.         // Enrich company rows with snapshot + subscription data for the view
  41.         $enriched = [];
  42.         foreach ($companies as $c) {
  43.             $appId = (int)($c['appId'] ?? 0);
  44.             $c['snapshot']     = $snapshots[$appId]     ?? [];
  45.             $c['subscription'] = $subscriptions[$appId] ?? null;
  46.             $enriched[]        = $c;
  47.         }
  48.         // Pre-compute the JS meta array so the Twig template does not need
  49.         // complex arrow-function expressions with nested filters.
  50.         $companiesJsMeta array_map(static function (array $c) {
  51.             return [
  52.                 'appId'       => (int)($c['appId'] ?? 0),
  53.                 'cacheStatus' => $c['snapshot']['_cache_status'] ?? 'pending',
  54.             ];
  55.         }, $enriched);
  56.         return $this->render('@CompanyGroup/pages/owner_dashboard/dashboard.html.twig', [
  57.             'page_title'         => 'My Companies',
  58.             'companies'          => $enriched,
  59.             'aggregate'          => $aggregate,
  60.             'kpis'               => $kpis,
  61.             'alerts'             => $alerts,
  62.             'subscriptions'      => $subscriptions,
  63.             'chart_data'         => $chartData,
  64.             'user_name'          => $request->getSession()->get(UserConstants::USER_NAME'Owner'),
  65.             'companies_js_meta'  => $companiesJsMeta,
  66.         ]);
  67.     }
  68.     // =========================================================================
  69.     // AJAX: LIVE KPI FETCH FOR ONE COMPANY
  70.     // =========================================================================
  71.     public function companyKpiFetchAction(Request $request$appId)
  72.     {
  73.         if (!$this->requireOwnerSession($request)) {
  74.             return new JsonResponse(['success' => false'message' => 'Not authenticated'], 401);
  75.         }
  76.         $userId = (int)$this->loggedUserId($request);
  77.         $appId  = (int)$appId;
  78.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  79.         $svc $this->get('app.owner_dashboard_service');
  80.         if (!$svc->userOwnsCompany($userId$appId)) {
  81.             return new JsonResponse(['success' => false'message' => 'Access denied'], 403);
  82.         }
  83.         $companies $svc->getOwnedCompanies($userId);
  84.         $company   null;
  85.         foreach ($companies as $c) {
  86.             if ((int)($c['appId'] ?? 0) === $appId) {
  87.                 $company $c;
  88.                 break;
  89.             }
  90.         }
  91.         if ($company === null) {
  92.             return new JsonResponse(['success' => false'message' => 'Company not found'], 404);
  93.         }
  94.         $forceRefresh = (bool)$request->query->get('refresh'false);
  95.         $snapshot $svc->fetchErpOwnerSnapshot($company$forceRefresh);
  96.         return new JsonResponse(['success' => true'data' => $snapshot]);
  97.     }
  98.     // =========================================================================
  99.     // SSO LAUNCH — redirect owner into company ERP
  100.     // =========================================================================
  101.     public function launchErpAction(Request $request$appId)
  102.     {
  103.         if (!$this->requireOwnerSession($request)) {
  104.             return $this->redirectToRoute('dashboard');
  105.         }
  106.         $userId = (int)$this->loggedUserId($request);
  107.         $appId  = (int)$appId;
  108.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  109.         $svc $this->get('app.owner_dashboard_service');
  110.         if (!$svc->userOwnsCompany($userId$appId)) {
  111.             $this->addFlash('error''You do not have access to that company.');
  112.             return $this->redirectToRoute('owner_dashboard');
  113.         }
  114.         $companies $svc->getOwnedCompanies($userId);
  115.         $company   null;
  116.         foreach ($companies as $c) {
  117.             if ((int)($c['appId'] ?? 0) === $appId) {
  118.                 $company $c;
  119.                 break;
  120.             }
  121.         }
  122.         if ($company === null) {
  123.             $this->addFlash('error''Company not found.');
  124.             return $this->redirectToRoute('owner_dashboard');
  125.         }
  126.         $erpUrl trim((string)($company['erpServerUrl'] ?? $company['erpServerAddress'] ?? ''));
  127.         if ($erpUrl === '') {
  128.             $this->addFlash('error''No ERP server address is configured for this company.');
  129.             return $this->redirectToRoute('owner_dashboard');
  130.         }
  131.         if (!preg_match('#^https?://#i'$erpUrl)) {
  132.             $erpUrl 'http://' $erpUrl;
  133.         }
  134.         $erpUrl rtrim($erpUrl'/');
  135.         $token $svc->generateSsoToken($company$userId);
  136.         // Redirect to the ERP's SSO entry point
  137.         $targetUrl $erpUrl '/central-sso?token=' urlencode($token)
  138.                    . '&uid=' $userId
  139.                    '&returnUrl=' urlencode($erpUrl '/dashboard');
  140.         return $this->redirect($targetUrl);
  141.     }
  142.     // =========================================================================
  143.     // SSO TOKEN VALIDATION API (called server-to-server by the ERP)
  144.     // No session requirement — this is a machine-to-machine call.
  145.     // =========================================================================
  146.     public function validateSsoTokenAction(Request $request)
  147.     {
  148.         $token = (string)$request->get('token''');
  149.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  150.         $svc $this->get('app.owner_dashboard_service');
  151.         $payload $svc->validateSsoToken($token);
  152.         if ($payload === null) {
  153.             return new JsonResponse(['success' => false'message' => 'Invalid or expired token'], 401);
  154.         }
  155.         // Look up the central user's email so the ERP can match a local account
  156.         $em    $this->getDoctrine()->getManager('company_group');
  157.         $email '';
  158.         $name  '';
  159.         try {
  160.             $user $em->getRepository('CompanyGroupBundle\Entity\EntityUser')
  161.                 ->find((int)($payload['central_user_id'] ?? 0));
  162.             if ($user) {
  163.                 $email = (string)$user->getEmail();
  164.                 $name  = (string)$user->getName();
  165.             }
  166.         } catch (\Exception $e) {
  167.             // Non-fatal
  168.         }
  169.         return new JsonResponse([
  170.             'success'          => true,
  171.             'central_user_id'  => $payload['central_user_id'],
  172.             'app_id'           => $payload['app_id'],
  173.             'email'            => $email,
  174.             'name'             => $name,
  175.             'expires_ts'       => $payload['expires_ts'],
  176.         ]);
  177.     }
  178.     // =========================================================================
  179.     // EXTEND SUBSCRIPTION
  180.     // =========================================================================
  181.     public function extendSubscriptionAction(Request $request$appId)
  182.     {
  183.         if (!$this->requireOwnerSession($request)) {
  184.             return $this->redirectToRoute('dashboard');
  185.         }
  186.         $userId = (int)$this->loggedUserId($request);
  187.         $appId  = (int)$appId;
  188.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  189.         $svc $this->get('app.owner_dashboard_service');
  190.         if (!$svc->userOwnsCompany($userId$appId)) {
  191.             $this->addFlash('error''Access denied.');
  192.             return $this->redirectToRoute('owner_dashboard');
  193.         }
  194.         $em           $this->getDoctrine()->getManager('company_group');
  195.         $companyGroup $em->getRepository('CompanyGroupBundle\Entity\CompanyGroup')
  196.             ->findOneBy(['appId' => $appId]);
  197.         if (!$companyGroup) {
  198.             $this->addFlash('error''Company not found.');
  199.             return $this->redirectToRoute('owner_dashboard');
  200.         }
  201.         $subscriptions $svc->getSubscriptionsForCompanies([$appId]);
  202.         $subscription  $subscriptions[$appId] ?? null;
  203.         if ($request->isMethod('POST')) {
  204.             $billingCycle $request->request->get('billing_cycle''monthly');
  205.             $planType     $request->request->get('plan_type''team');
  206.             $normalUsers  max(0, (int)$request->request->get('normal_user_count'0));
  207.             $adminUsers   max(0, (int)$request->request->get('admin_user_count'0));
  208.             $mlUsers      max(0, (int)$request->request->get('ml_user_count'0));
  209.             /** @var \CompanyGroupBundle\Modules\Api\Service\QuoteService $quoteSvc */
  210.             $quoteSvc $this->get('app.quote_service');
  211.             $quote $quoteSvc->createCustomerQuote([
  212.                 'app_id'              => $appId,
  213.                 'plan_type'           => $planType,
  214.                 'billing_cycle'       => $billingCycle,
  215.                 'payment_type'        => 'manual',
  216.                 'normal_user_count'   => $normalUsers,
  217.                 'admin_user_count'    => $adminUsers,
  218.                 'ml_user_count'       => $mlUsers,
  219.                 'customer_email'      => $companyGroup->getEmail() ?? '',
  220.                 'customer_name'       => $companyGroup->getName() ?? '',
  221.                 'company_name'        => $companyGroup->getName() ?? '',
  222.                 'customer_notes'      => 'Subscription extension requested from Owner Dashboard',
  223.             ]);
  224.             $this->addFlash('success''Extension request submitted. Our team will confirm shortly.');
  225.             return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
  226.         }
  227.         /** @var \CompanyGroupBundle\Modules\Api\Service\PricingService $pricingSvc */
  228.         $pricingSvc $this->get('app.pricing_service');
  229.         $currentPlan  $subscription['package_type'] ?? $companyGroup->getPackageType() ?? 'team';
  230.         $normalUsers  = (int)($companyGroup->getUserAllowed() ?? 5);
  231.         $adminUsers   = (int)($companyGroup->getAdminUserAllowed() ?? 1);
  232.         $breakdown    $pricingSvc->getPriceBreakdown($normalUsers$adminUsers0'monthly'$currentPlan);
  233.         return $this->render('@CompanyGroup/pages/owner_dashboard/extend_subscription.html.twig', [
  234.             'page_title'    => 'Extend Subscription',
  235.             'company'       => $companyGroup,
  236.             'subscription'  => $subscription,
  237.             'breakdown'     => $breakdown,
  238.             'app_id'        => $appId,
  239.             'current_plan'  => $currentPlan,
  240.             'normal_users'  => $normalUsers,
  241.             'admin_users'   => $adminUsers,
  242.         ]);
  243.     }
  244.     // =========================================================================
  245.     // ADD USER SEATS
  246.     // =========================================================================
  247.     public function addUsersAction(Request $request$appId)
  248.     {
  249.         if (!$this->requireOwnerSession($request)) {
  250.             return $this->redirectToRoute('dashboard');
  251.         }
  252.         $userId = (int)$this->loggedUserId($request);
  253.         $appId  = (int)$appId;
  254.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  255.         $svc $this->get('app.owner_dashboard_service');
  256.         if (!$svc->userOwnsCompany($userId$appId)) {
  257.             $this->addFlash('error''Access denied.');
  258.             return $this->redirectToRoute('owner_dashboard');
  259.         }
  260.         $em           $this->getDoctrine()->getManager('company_group');
  261.         $companyGroup $em->getRepository('CompanyGroupBundle\Entity\CompanyGroup')
  262.             ->findOneBy(['appId' => $appId]);
  263.         if (!$companyGroup) {
  264.             $this->addFlash('error''Company not found.');
  265.             return $this->redirectToRoute('owner_dashboard');
  266.         }
  267.         $subscriptions $svc->getSubscriptionsForCompanies([$appId]);
  268.         $subscription  $subscriptions[$appId] ?? null;
  269.         $currentPlan   $subscription['package_type'] ?? $companyGroup->getPackageType() ?? 'team';
  270.         if ($request->isMethod('POST')) {
  271.             $addNormal max(0, (int)$request->request->get('add_normal_users'0));
  272.             $addAdmin  max(0, (int)$request->request->get('add_admin_users',  0));
  273.             $addMl     max(0, (int)$request->request->get('add_ml_users',     0));
  274.             if ($addNormal $addAdmin $addMl <= 0) {
  275.                 $this->addFlash('error''Please select at least one seat to add.');
  276.                 return $this->redirectToRoute('owner_add_users', ['appId' => $appId]);
  277.             }
  278.             $totalNormal = (int)($companyGroup->getUserAllowed() ?? 0) + $addNormal;
  279.             $totalAdmin  = (int)($companyGroup->getAdminUserAllowed() ?? 0) + $addAdmin;
  280.             $totalMl     $addMl;
  281.             /** @var \CompanyGroupBundle\Modules\Api\Service\QuoteService $quoteSvc */
  282.             $quoteSvc $this->get('app.quote_service');
  283.             $quote $quoteSvc->createCustomerQuote([
  284.                 'app_id'              => $appId,
  285.                 'plan_type'           => $currentPlan,
  286.                 'billing_cycle'       => $subscription['billing_cycle'] ?? 'monthly',
  287.                 'payment_type'        => 'manual',
  288.                 'normal_user_count'   => $totalNormal,
  289.                 'admin_user_count'    => $totalAdmin,
  290.                 'ml_user_count'       => $totalMl,
  291.                 'customer_email'      => $companyGroup->getEmail() ?? '',
  292.                 'customer_name'       => $companyGroup->getName() ?? '',
  293.                 'company_name'        => $companyGroup->getName() ?? '',
  294.                 'customer_notes'      => "Add-seats request from Owner Dashboard: +{$addNormal} normal, +{$addAdmin} admin, +{$addMl} ML",
  295.             ]);
  296.             $this->addFlash('success''Seat request submitted. Our team will confirm and provision shortly.');
  297.             return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
  298.         }
  299.         /** @var \CompanyGroupBundle\Modules\Api\Service\PricingService $pricingSvc */
  300.         $pricingSvc $this->get('app.pricing_service');
  301.         $breakdown  $pricingSvc->getPriceBreakdown(
  302.             (int)($companyGroup->getUserAllowed() ?? 5),
  303.             (int)($companyGroup->getAdminUserAllowed() ?? 1),
  304.             0,
  305.             $subscription['billing_cycle'] ?? 'monthly',
  306.             $currentPlan
  307.         );
  308.         return $this->render('@CompanyGroup/pages/owner_dashboard/add_users.html.twig', [
  309.             'page_title'    => 'Add User Seats',
  310.             'company'       => $companyGroup,
  311.             'subscription'  => $subscription,
  312.             'breakdown'     => $breakdown,
  313.             'app_id'        => $appId,
  314.             'current_plan'  => $currentPlan,
  315.             'current_normal'=> (int)($companyGroup->getUserAllowed() ?? 0),
  316.             'current_admin' => (int)($companyGroup->getAdminUserAllowed() ?? 0),
  317.         ]);
  318.     }
  319.     // =========================================================================
  320.     // COMPANY SETTINGS
  321.     // =========================================================================
  322.     public function companySettingsAction(Request $request$appId)
  323.     {
  324.         if (!$this->requireOwnerSession($request)) {
  325.             return $this->redirectToRoute('dashboard');
  326.         }
  327.         $userId = (int)$this->loggedUserId($request);
  328.         $appId  = (int)$appId;
  329.         /** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
  330.         $svc $this->get('app.owner_dashboard_service');
  331.         if (!$svc->userOwnsCompany($userId$appId)) {
  332.             $this->addFlash('error''Access denied.');
  333.             return $this->redirectToRoute('owner_dashboard');
  334.         }
  335.         $em           $this->getDoctrine()->getManager('company_group');
  336.         $companyGroup $em->getRepository('CompanyGroupBundle\Entity\CompanyGroup')
  337.             ->findOneBy(['appId' => $appId]);
  338.         if (!$companyGroup) {
  339.             $this->addFlash('error''Company not found.');
  340.             return $this->redirectToRoute('owner_dashboard');
  341.         }
  342.         if ($request->isMethod('POST')) {
  343.             $name           trim((string)$request->request->get('name'''));
  344.             $email          trim((string)$request->request->get('email'''));
  345.             $contactNumber  trim((string)$request->request->get('contact_number'''));
  346.             $address        trim((string)$request->request->get('address'''));
  347.             $billingAddress trim((string)$request->request->get('billing_address'''));
  348.             $motto          trim((string)$request->request->get('motto'''));
  349.             $invoiceFooter  trim((string)$request->request->get('invoice_footer'''));
  350.             if ($name !== '') {
  351.                 $companyGroup->setName($name);
  352.             }
  353.             if ($email !== '') {
  354.                 $companyGroup->setEmail($email);
  355.             }
  356.             $companyGroup->setContactNumber($contactNumber !== '' $contactNumber null);
  357.             $companyGroup->setAddress($address !== '' $address null);
  358.             $companyGroup->setBillingAddress($billingAddress !== '' $billingAddress null);
  359.             $companyGroup->setMotto($motto !== '' $motto null);
  360.             $companyGroup->setInvoiceFooter($invoiceFooter !== '' $invoiceFooter null);
  361.             $em->flush();
  362.             $this->addFlash('success''Company settings saved successfully.');
  363.             return $this->redirectToRoute('owner_company_settings', ['appId' => $appId]);
  364.         }
  365.         return $this->render('@CompanyGroup/pages/owner_dashboard/company_settings.html.twig', [
  366.             'page_title'    => 'Company Settings — ' $companyGroup->getName(),
  367.             'company'       => $companyGroup,
  368.             'app_id'        => $appId,
  369.         ]);
  370.     }
  371.     // =========================================================================
  372.     // PRIVATE HELPERS
  373.     // =========================================================================
  374.     /**
  375.      * Returns true if a valid owner session exists. Does NOT redirect — the
  376.      * caller decides the redirect target.
  377.      */
  378.     private function requireOwnerSession(Request $request): bool
  379.     {
  380.         $userId = (int)$request->getSession()->get(UserConstants::USER_ID0);
  381.         return $userId 0;
  382.     }
  383. }