<?php
namespace CompanyGroupBundle\Controller;
use ApplicationBundle\Interfaces\SessionCheckInterface;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* OwnerDashboardController
*
* Company Owner Dashboard — central-server view for a business owner managing
* multiple companies. All routes are under /my/.
*
* Auth guard: every action calls requireOwnerSession() which redirects to
* 'dashboard' (the ERP login) if the user is not logged in on the central server.
*/
class OwnerDashboardController extends CompanyGroupGenericController implements SessionCheckInterface
{
// =========================================================================
// MAIN DASHBOARD
// =========================================================================
public function dashboardAction(Request $request)
{
if (!$this->requireOwnerSession($request)) {
return $this->redirectToRoute('dashboard');
}
$userId = (int)$this->loggedUserId($request);
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
$companies = $svc->getOwnedCompanies($userId);
$appIds = array_column($companies, 'appId');
$subscriptions = $svc->getSubscriptionsForCompanies(array_map('intval', $appIds));
$chartData = $svc->getRevenueChartData(array_map('intval', $appIds), 6);
// Load ERP snapshots from cache (stale is fine for initial page load —
// the page JS will trigger per-company refresh for the live KPI cards).
$snapshots = $svc->fetchAllErpSnapshots($companies, false);
$aggregate = $svc->aggregateFinancials($companies, $snapshots);
$kpis = $svc->aggregateKpis($snapshots);
$alerts = $svc->buildAlerts($companies, $snapshots, $subscriptions);
// Enrich company rows with snapshot + subscription data for the view
$enriched = [];
foreach ($companies as $c) {
$appId = (int)($c['appId'] ?? 0);
$c['snapshot'] = $snapshots[$appId] ?? [];
$c['subscription'] = $subscriptions[$appId] ?? null;
$enriched[] = $c;
}
// Pre-compute the JS meta array so the Twig template does not need
// complex arrow-function expressions with nested filters.
$companiesJsMeta = array_map(static function (array $c) {
return [
'appId' => (int)($c['appId'] ?? 0),
'cacheStatus' => $c['snapshot']['_cache_status'] ?? 'pending',
];
}, $enriched);
return $this->render('@CompanyGroup/pages/owner_dashboard/dashboard.html.twig', [
'page_title' => 'My Companies',
'companies' => $enriched,
'aggregate' => $aggregate,
'kpis' => $kpis,
'alerts' => $alerts,
'subscriptions' => $subscriptions,
'chart_data' => $chartData,
'user_name' => $request->getSession()->get(UserConstants::USER_NAME, 'Owner'),
'companies_js_meta' => $companiesJsMeta,
]);
}
// =========================================================================
// AJAX: LIVE KPI FETCH FOR ONE COMPANY
// =========================================================================
public function companyKpiFetchAction(Request $request, $appId)
{
if (!$this->requireOwnerSession($request)) {
return new JsonResponse(['success' => false, 'message' => 'Not authenticated'], 401);
}
$userId = (int)$this->loggedUserId($request);
$appId = (int)$appId;
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
if (!$svc->userOwnsCompany($userId, $appId)) {
return new JsonResponse(['success' => false, 'message' => 'Access denied'], 403);
}
$companies = $svc->getOwnedCompanies($userId);
$company = null;
foreach ($companies as $c) {
if ((int)($c['appId'] ?? 0) === $appId) {
$company = $c;
break;
}
}
if ($company === null) {
return new JsonResponse(['success' => false, 'message' => 'Company not found'], 404);
}
$forceRefresh = (bool)$request->query->get('refresh', false);
$snapshot = $svc->fetchErpOwnerSnapshot($company, $forceRefresh);
return new JsonResponse(['success' => true, 'data' => $snapshot]);
}
// =========================================================================
// SSO LAUNCH — redirect owner into company ERP
// =========================================================================
public function launchErpAction(Request $request, $appId)
{
if (!$this->requireOwnerSession($request)) {
return $this->redirectToRoute('dashboard');
}
$userId = (int)$this->loggedUserId($request);
$appId = (int)$appId;
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
if (!$svc->userOwnsCompany($userId, $appId)) {
$this->addFlash('error', 'You do not have access to that company.');
return $this->redirectToRoute('owner_dashboard');
}
$companies = $svc->getOwnedCompanies($userId);
$company = null;
foreach ($companies as $c) {
if ((int)($c['appId'] ?? 0) === $appId) {
$company = $c;
break;
}
}
if ($company === null) {
$this->addFlash('error', 'Company not found.');
return $this->redirectToRoute('owner_dashboard');
}
$erpUrl = trim((string)($company['erpServerUrl'] ?? $company['erpServerAddress'] ?? ''));
if ($erpUrl === '') {
$this->addFlash('error', 'No ERP server address is configured for this company.');
return $this->redirectToRoute('owner_dashboard');
}
if (!preg_match('#^https?://#i', $erpUrl)) {
$erpUrl = 'http://' . $erpUrl;
}
$erpUrl = rtrim($erpUrl, '/');
$token = $svc->generateSsoToken($company, $userId);
// Redirect to the ERP's SSO entry point
$targetUrl = $erpUrl . '/central-sso?token=' . urlencode($token)
. '&uid=' . $userId
. '&returnUrl=' . urlencode($erpUrl . '/dashboard');
return $this->redirect($targetUrl);
}
// =========================================================================
// SSO TOKEN VALIDATION API (called server-to-server by the ERP)
// No session requirement — this is a machine-to-machine call.
// =========================================================================
public function validateSsoTokenAction(Request $request)
{
$token = (string)$request->get('token', '');
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
$payload = $svc->validateSsoToken($token);
if ($payload === null) {
return new JsonResponse(['success' => false, 'message' => 'Invalid or expired token'], 401);
}
// Look up the central user's email so the ERP can match a local account
$em = $this->getDoctrine()->getManager('company_group');
$email = '';
$name = '';
try {
$user = $em->getRepository('CompanyGroupBundle\Entity\EntityUser')
->find((int)($payload['central_user_id'] ?? 0));
if ($user) {
$email = (string)$user->getEmail();
$name = (string)$user->getName();
}
} catch (\Exception $e) {
// Non-fatal
}
return new JsonResponse([
'success' => true,
'central_user_id' => $payload['central_user_id'],
'app_id' => $payload['app_id'],
'email' => $email,
'name' => $name,
'expires_ts' => $payload['expires_ts'],
]);
}
// =========================================================================
// EXTEND SUBSCRIPTION
// =========================================================================
public function extendSubscriptionAction(Request $request, $appId)
{
if (!$this->requireOwnerSession($request)) {
return $this->redirectToRoute('dashboard');
}
$userId = (int)$this->loggedUserId($request);
$appId = (int)$appId;
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
if (!$svc->userOwnsCompany($userId, $appId)) {
$this->addFlash('error', 'Access denied.');
return $this->redirectToRoute('owner_dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$companyGroup = $em->getRepository('CompanyGroupBundle\Entity\CompanyGroup')
->findOneBy(['appId' => $appId]);
if (!$companyGroup) {
$this->addFlash('error', 'Company not found.');
return $this->redirectToRoute('owner_dashboard');
}
$subscriptions = $svc->getSubscriptionsForCompanies([$appId]);
$subscription = $subscriptions[$appId] ?? null;
if ($request->isMethod('POST')) {
$billingCycle = $request->request->get('billing_cycle', 'monthly');
$planType = $request->request->get('plan_type', 'team');
$normalUsers = max(0, (int)$request->request->get('normal_user_count', 0));
$adminUsers = max(0, (int)$request->request->get('admin_user_count', 0));
$mlUsers = max(0, (int)$request->request->get('ml_user_count', 0));
/** @var \CompanyGroupBundle\Modules\Api\Service\QuoteService $quoteSvc */
$quoteSvc = $this->get('app.quote_service');
$quote = $quoteSvc->createCustomerQuote([
'app_id' => $appId,
'plan_type' => $planType,
'billing_cycle' => $billingCycle,
'payment_type' => 'manual',
'normal_user_count' => $normalUsers,
'admin_user_count' => $adminUsers,
'ml_user_count' => $mlUsers,
'customer_email' => $companyGroup->getEmail() ?? '',
'customer_name' => $companyGroup->getName() ?? '',
'company_name' => $companyGroup->getName() ?? '',
'customer_notes' => 'Subscription extension requested from Owner Dashboard',
]);
$this->addFlash('success', 'Extension request submitted. Our team will confirm shortly.');
return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
}
/** @var \CompanyGroupBundle\Modules\Api\Service\PricingService $pricingSvc */
$pricingSvc = $this->get('app.pricing_service');
$currentPlan = $subscription['package_type'] ?? $companyGroup->getPackageType() ?? 'team';
$normalUsers = (int)($companyGroup->getUserAllowed() ?? 5);
$adminUsers = (int)($companyGroup->getAdminUserAllowed() ?? 1);
$breakdown = $pricingSvc->getPriceBreakdown($normalUsers, $adminUsers, 0, 'monthly', $currentPlan);
return $this->render('@CompanyGroup/pages/owner_dashboard/extend_subscription.html.twig', [
'page_title' => 'Extend Subscription',
'company' => $companyGroup,
'subscription' => $subscription,
'breakdown' => $breakdown,
'app_id' => $appId,
'current_plan' => $currentPlan,
'normal_users' => $normalUsers,
'admin_users' => $adminUsers,
]);
}
// =========================================================================
// ADD USER SEATS
// =========================================================================
public function addUsersAction(Request $request, $appId)
{
if (!$this->requireOwnerSession($request)) {
return $this->redirectToRoute('dashboard');
}
$userId = (int)$this->loggedUserId($request);
$appId = (int)$appId;
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
if (!$svc->userOwnsCompany($userId, $appId)) {
$this->addFlash('error', 'Access denied.');
return $this->redirectToRoute('owner_dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$companyGroup = $em->getRepository('CompanyGroupBundle\Entity\CompanyGroup')
->findOneBy(['appId' => $appId]);
if (!$companyGroup) {
$this->addFlash('error', 'Company not found.');
return $this->redirectToRoute('owner_dashboard');
}
$subscriptions = $svc->getSubscriptionsForCompanies([$appId]);
$subscription = $subscriptions[$appId] ?? null;
$currentPlan = $subscription['package_type'] ?? $companyGroup->getPackageType() ?? 'team';
if ($request->isMethod('POST')) {
$addNormal = max(0, (int)$request->request->get('add_normal_users', 0));
$addAdmin = max(0, (int)$request->request->get('add_admin_users', 0));
$addMl = max(0, (int)$request->request->get('add_ml_users', 0));
if ($addNormal + $addAdmin + $addMl <= 0) {
$this->addFlash('error', 'Please select at least one seat to add.');
return $this->redirectToRoute('owner_add_users', ['appId' => $appId]);
}
$totalNormal = (int)($companyGroup->getUserAllowed() ?? 0) + $addNormal;
$totalAdmin = (int)($companyGroup->getAdminUserAllowed() ?? 0) + $addAdmin;
$totalMl = $addMl;
/** @var \CompanyGroupBundle\Modules\Api\Service\QuoteService $quoteSvc */
$quoteSvc = $this->get('app.quote_service');
$quote = $quoteSvc->createCustomerQuote([
'app_id' => $appId,
'plan_type' => $currentPlan,
'billing_cycle' => $subscription['billing_cycle'] ?? 'monthly',
'payment_type' => 'manual',
'normal_user_count' => $totalNormal,
'admin_user_count' => $totalAdmin,
'ml_user_count' => $totalMl,
'customer_email' => $companyGroup->getEmail() ?? '',
'customer_name' => $companyGroup->getName() ?? '',
'company_name' => $companyGroup->getName() ?? '',
'customer_notes' => "Add-seats request from Owner Dashboard: +{$addNormal} normal, +{$addAdmin} admin, +{$addMl} ML",
]);
$this->addFlash('success', 'Seat request submitted. Our team will confirm and provision shortly.');
return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
}
/** @var \CompanyGroupBundle\Modules\Api\Service\PricingService $pricingSvc */
$pricingSvc = $this->get('app.pricing_service');
$breakdown = $pricingSvc->getPriceBreakdown(
(int)($companyGroup->getUserAllowed() ?? 5),
(int)($companyGroup->getAdminUserAllowed() ?? 1),
0,
$subscription['billing_cycle'] ?? 'monthly',
$currentPlan
);
return $this->render('@CompanyGroup/pages/owner_dashboard/add_users.html.twig', [
'page_title' => 'Add User Seats',
'company' => $companyGroup,
'subscription' => $subscription,
'breakdown' => $breakdown,
'app_id' => $appId,
'current_plan' => $currentPlan,
'current_normal'=> (int)($companyGroup->getUserAllowed() ?? 0),
'current_admin' => (int)($companyGroup->getAdminUserAllowed() ?? 0),
]);
}
// =========================================================================
// COMPANY SETTINGS
// =========================================================================
public function companySettingsAction(Request $request, $appId)
{
if (!$this->requireOwnerSession($request)) {
return $this->redirectToRoute('dashboard');
}
$userId = (int)$this->loggedUserId($request);
$appId = (int)$appId;
/** @var \CompanyGroupBundle\Modules\Api\Service\OwnerDashboardService $svc */
$svc = $this->get('app.owner_dashboard_service');
if (!$svc->userOwnsCompany($userId, $appId)) {
$this->addFlash('error', 'Access denied.');
return $this->redirectToRoute('owner_dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$companyGroup = $em->getRepository('CompanyGroupBundle\Entity\CompanyGroup')
->findOneBy(['appId' => $appId]);
if (!$companyGroup) {
$this->addFlash('error', 'Company not found.');
return $this->redirectToRoute('owner_dashboard');
}
if ($request->isMethod('POST')) {
$name = trim((string)$request->request->get('name', ''));
$email = trim((string)$request->request->get('email', ''));
$contactNumber = trim((string)$request->request->get('contact_number', ''));
$address = trim((string)$request->request->get('address', ''));
$billingAddress = trim((string)$request->request->get('billing_address', ''));
$motto = trim((string)$request->request->get('motto', ''));
$invoiceFooter = trim((string)$request->request->get('invoice_footer', ''));
if ($name !== '') {
$companyGroup->setName($name);
}
if ($email !== '') {
$companyGroup->setEmail($email);
}
$companyGroup->setContactNumber($contactNumber !== '' ? $contactNumber : null);
$companyGroup->setAddress($address !== '' ? $address : null);
$companyGroup->setBillingAddress($billingAddress !== '' ? $billingAddress : null);
$companyGroup->setMotto($motto !== '' ? $motto : null);
$companyGroup->setInvoiceFooter($invoiceFooter !== '' ? $invoiceFooter : null);
$em->flush();
$this->addFlash('success', 'Company settings saved successfully.');
return $this->redirectToRoute('owner_company_settings', ['appId' => $appId]);
}
return $this->render('@CompanyGroup/pages/owner_dashboard/company_settings.html.twig', [
'page_title' => 'Company Settings — ' . $companyGroup->getName(),
'company' => $companyGroup,
'app_id' => $appId,
]);
}
// =========================================================================
// PRIVATE HELPERS
// =========================================================================
/**
* Returns true if a valid owner session exists. Does NOT redirect — the
* caller decides the redirect target.
*/
private function requireOwnerSession(Request $request): bool
{
$userId = (int)$request->getSession()->get(UserConstants::USER_ID, 0);
return $userId > 0;
}
}