<?php
namespace CompanyGroupBundle\Controller;
use ApplicationBundle\Constants\ModuleConstant;
use ApplicationBundle\Modules\System\MiscActions;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use ApplicationBundle\Command\Support\FleetHealthVerdict;
use ApplicationBundle\Helper\EbBridgeConfig;
use ApplicationBundle\Modules\Api\Support\EbBridgeVerifier;
use CompanyGroupBundle\Entity\EnergyBridgeSiteLink;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
class SuperAdminDashboardController extends Controller
{
public function indexAction(Request $request)
{
if (!$this->canAccessSuperAdminDashboard($request)) {
return $this->redirectToRoute('dashboard');
}
$service = $this->get('app.admin_dashboard_service');
$metrics = $service->getMetrics();
$companies = $service->listCompanies(12, 0, []);
$usageSummary = $service->getUsageSummary(30);
$alerts = $service->getAlerts();
$chartData = $this->buildChartData($usageSummary);
return $this->render('@CompanyGroup/pages/super_admin_command_center.html.twig', [
'page_title' => 'Super Admin Command Center',
'metrics' => $metrics,
'companies' => $companies,
'usage_summary' => $usageSummary,
'alerts' => $alerts,
'chart_data' => $chartData,
]);
}
public function companyListAction(Request $request)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_') {
return $this->redirectToRoute('dashboard');
}
if (!$this->canAccessSuperAdminDashboard($request)) {
return $this->redirectToRoute('dashboard');
}
$page = max(1, (int)$request->query->get('page', 1));
$limit = 25;
$offset = ($page - 1) * $limit;
$filters = array(
'search' => trim((string)$request->query->get('q', '')),
'status' => trim((string)$request->query->get('status', '')),
);
$service = $this->get('app.admin_dashboard_service');
$companies = $service->listCompanies($limit, $offset, $filters);
$total = $this->countCompaniesForAdminList($filters);
$totalPages = max(1, (int)ceil($total / $limit));
$summary = $this->getCompanyListSummary();
return $this->render('@CompanyGroup/pages/admin/companies/list_companies.html.twig', array(
'page_title' => 'Companies',
'companies' => $companies,
'filters' => $filters,
'summary' => $summary,
'total' => $total,
'currentPage' => $page,
'totalPages' => $totalPages,
));
}
/**
* L4 — Fleet health dashboard: 100 tenants at a glance. Reads the central snapshots the
* collector wrote (never live-queries tenant DBs) and renders one green/amber/red row per
* tenant, red-first. Every central read is guarded so the page renders even before the
* fleet_health_snapshot / tenant_schema_state / cron_cycle_run tables are migrated.
*/
public function fleetHealthAction(Request $request)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_') {
return $this->redirectToRoute('dashboard');
}
if (!$this->canAccessSuperAdminDashboard($request)) {
return $this->redirectToRoute('dashboard');
}
$cem = $this->getDoctrine()->getManager('company_group');
$now = time();
$graceDays = $this->container->hasParameter('subscription_grace_days')
? (int) $this->container->getParameter('subscription_grace_days') : 7;
$tenants = array();
try {
foreach ($cem->getRepository('CompanyGroupBundle\\Entity\\CompanyGroup')->findBy(array('active' => 1)) as $g) {
if (!$g->getDbName()) { continue; }
$tenants[(int) $g->getAppId()] = array('appId' => (int) $g->getAppId(), 'name' => $g->getName(), 'dbName' => $g->getDbName());
}
} catch (\Throwable $e) { /* central unreachable */ }
$snaps = array();
try { foreach ($cem->getRepository('CompanyGroupBundle\\Entity\\FleetHealthSnapshot')->findAll() as $s) { $snaps[(int) $s->getAppId()] = $s; } } catch (\Throwable $e) {}
$schema = array();
try { foreach ($cem->getRepository('CompanyGroupBundle\\Entity\\TenantSchemaState')->findAll() as $s) { $schema[(int) $s->getAppId()] = $s; } } catch (\Throwable $e) {}
// Latest fan-out cron cycle (fleet-wide), latest fleet-migrate report, recent alerts.
$lastCron = null; $lastMigrate = null; $alerts = array();
try {
foreach ($cem->getRepository('CompanyGroupBundle\\Entity\\CronCycleRun')->findBy(array(), array('finishedAt' => 'DESC'), 30) as $c) {
if ($c->getCommand() === 'fleet-migrate') { if ($lastMigrate === null) { $lastMigrate = $c; } }
elseif ($lastCron === null) { $lastCron = $c; }
if ($c->getAlert() && count($alerts) < 8) { $alerts[] = $c; }
}
} catch (\Throwable $e) {}
$cronAgeSec = null; $cronLastFailed = false;
if ($lastCron && $lastCron->getFinishedAt() instanceof \DateTimeInterface) {
$cronAgeSec = $now - (int) $lastCron->getFinishedAt()->format('U');
$cronLastFailed = ((int) $lastCron->getFailedCount() + (int) $lastCron->getTimedOutCount()) > 0;
}
// FE1 — recent red-finding alerts (pre-schema-safe: table missing → empty panel).
$fleetAlerts = array();
try {
$fleetAlerts = $cem->getRepository('CompanyGroupBundle\\Entity\\PlatformFleetAlert')
->findBy(array(), array('id' => 'DESC'), 10);
} catch (\Throwable $e) {}
$rows = array();
$counts = array('ok' => 0, 'warn' => 0, 'fail' => 0, 'unknown' => 0);
$oldest = null;
foreach ($tenants as $appId => $t) {
$s = isset($snaps[$appId]) ? $snaps[$appId] : null;
$sc = isset($schema[$appId]) ? $schema[$appId] : null;
$collectedTs = ($s && $s->getCollectedAt() instanceof \DateTimeInterface) ? (int) $s->getCollectedAt()->format('U') : 0;
$facts = array(
'collectedAtTs' => $collectedTs, 'collectError' => $s ? $s->getCollectError() : null,
'tbBalanced' => $s ? $s->getTbBalanced() : null, 'tbDiff' => $s ? $s->getTbDiff() : null,
'auditChainOk' => $s ? $s->getAuditChainOk() : null, 'auditChainBreaks' => $s ? (int) $s->getAuditChainBreaks() : 0,
'unbalancedVouchers' => $s ? $s->getUnbalancedVouchers() : null,
'markerUncovered' => $s ? $s->getMarkerUncovered() : null,
'schemaStatus' => $sc ? $sc->getStatus() : null,
'cronAgeSec' => $cronAgeSec, 'cronLastFailed' => $cronLastFailed,
'subscriptionExpiryTs' => $s ? (int) $s->getSubscriptionExpiryTs() : 0, 'graceDays' => $graceDays,
'activityLogRows' => $s ? $s->getActivityLogRows() : null,
'jsErrorsToday' => $s ? $s->getJsErrorsToday() : null,
'smokeChecked' => $s ? $s->getSmokeChecked() : null,
'smokeFailed' => $s ? $s->getSmokeFailed() : null,
'smokeAssertFail' => $s ? $s->getSmokeAssertFail() : null,
);
$v = FleetHealthVerdict::evaluate($facts, $now);
$counts[$v['overall']] = (isset($counts[$v['overall']]) ? $counts[$v['overall']] : 0) + 1;
if ($collectedTs > 0 && ($oldest === null || $collectedTs < $oldest)) { $oldest = $collectedTs; }
// FE3 — 7-day JS-error trend (collector-computed JSON; null pre-migration).
$jsTrend = null;
if ($s && $s->getJsErrors7d()) {
$decoded = json_decode((string) $s->getJsErrors7d(), true);
if (is_array($decoded) && isset($decoded['c']) && is_array($decoded['c']) && !empty($decoded['c'])) {
$trendCounts = array_map('intval', $decoded['c']);
$jsTrend = array('counts' => $trendCounts,
'dates' => isset($decoded['d']) && is_array($decoded['d']) ? $decoded['d'] : array(),
'max' => max(1, max($trendCounts)));
}
}
$rows[] = array('t' => $t, 'verdict' => $v, 'dbSizeMb' => $s ? $s->getDbSizeMb() : null,
'collectedTs' => $collectedTs, 'schemaVersion' => $sc ? substr((string) $sc->getCurrentVersion(), 0, 10) : null,
'jsTrend' => $jsTrend, 'jsToday' => $s ? $s->getJsErrorsToday() : null);
}
// Red-first: fail > warn > unknown > ok.
$rank = array('fail' => 0, 'warn' => 1, 'unknown' => 2, 'ok' => 3);
usort($rows, function ($a, $b) use ($rank) {
$ra = isset($rank[$a['verdict']['overall']]) ? $rank[$a['verdict']['overall']] : 9;
$rb = isset($rank[$b['verdict']['overall']]) ? $rank[$b['verdict']['overall']] : 9;
if ($ra !== $rb) { return $ra - $rb; }
return $a['t']['appId'] - $b['t']['appId'];
});
return $this->render('@CompanyGroup/pages/admin/fleet_health.html.twig', array(
'page_title' => 'Fleet Health',
'rows' => $rows,
'counts' => $counts,
'tenantCount' => count($tenants),
'oldestAgeSec' => $oldest ? ($now - $oldest) : null,
'lastCron' => $lastCron,
'lastMigrate' => $lastMigrate,
'alerts' => $alerts,
'fleetAlerts' => $fleetAlerts,
'checkKeys' => array('tb' => 'TB', 'chain' => 'Chain', 'vouchers' => 'Vouchers', 'markers' => 'Markers',
'schema' => 'Schema', 'cron' => 'Cron', 'subscription' => 'Subscription', 'smoke' => 'Smoke',
'jserrors' => 'JS Err', 'activity' => 'Activity', 'snapshot' => 'Snapshot'),
));
}
/** L4 — re-collect ONE tenant on demand (out-of-process, so the web connection is untouched). */
public function fleetHealthRefreshAction(Request $request, $appId)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_' || !$this->canAccessSuperAdminDashboard($request)) {
return $this->redirectToRoute('dashboard');
}
try {
$projectDir = $this->container->hasParameter('kernel.project_dir')
? $this->container->getParameter('kernel.project_dir') : dirname($this->container->getParameter('kernel.root_dir'));
$env = $this->container->getParameter('kernel.environment');
$p = new \Symfony\Component\Process\Process(array(
PHP_BINARY, $projectDir . '/bin/console', 'inno:for-each-tenant', 'inno:fleet-health-collect',
'--only=' . (int) $appId, '--env=' . $env, '--no-interaction'), $projectDir, null, null, 120);
$p->run();
$this->addFlash('success', 'Re-collected tenant ' . (int) $appId . '.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Refresh failed: ' . $e->getMessage());
}
return $this->redirectToRoute('fleet_health');
}
public function companyViewAction(Request $request, $appId)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_') {
return $this->redirectToRoute('dashboard');
}
if (!$this->canAccessSuperAdminDashboard($request)) {
return $this->redirectToRoute('dashboard');
}
$service = $this->get('app.admin_dashboard_service');
$company = $service->getCompanyByAppId((int)$appId);
if (!$company) {
throw $this->createNotFoundException('Company appId #' . (int)$appId . ' not found.');
}
$em = $this->getDoctrine()->getManager('company_group');
$companyEntity = $em->getRepository('CompanyGroupBundle\\Entity\\CompanyGroup')->findOneBy(array(
'appId' => (int)$appId,
));
$enabledModuleIds = $companyEntity ? $this->parseCompanyModuleIdList($companyEntity->getEnabledModuleIdList()) : array();
if (empty($enabledModuleIds)) {
$enabledModuleIds = $this->getDefaultEnabledCompanyModuleIds();
}
return $this->render('@CompanyGroup/pages/admin/companies/view_company.html.twig', array(
'page_title' => 'Company Details',
'company' => $company,
'company_entity' => $companyEntity,
'enabled_count' => count($enabledModuleIds),
'module_count' => count(ModuleConstant::$moduleList),
));
}
private function canAccessSuperAdminDashboard(Request $request)
{
$session = $request->getSession();
$userId = (int) $session->get(UserConstants::USER_ID, 0);
if ($userId <= 0) {
return false;
}
$userType = (int) $session->get(UserConstants::USER_TYPE, 0);
$isBuddybeeAdmin = (int) $session->get(UserConstants::IS_BUDDYBEE_ADMIN, 0);
$allModuleAccess = (int) $session->get(UserConstants::ALL_MODULE_ACCESS_FLAG, 0);
$allowedTypes = [
UserConstants::USER_TYPE_SYSTEM,
UserConstants::USER_TYPE_MANAGEMENT_USER,
UserConstants::USER_TYPE_GENERAL,
];
return $isBuddybeeAdmin === 1 || $allModuleAccess === 1 || in_array($userType, $allowedTypes, true);
}
private function countCompaniesForAdminList(array $filters)
{
$conn = $this->getDoctrine()->getManager('company_group')->getConnection();
$where = array();
$params = array();
if (!empty($filters['search'])) {
$where[] = '(name LIKE :search OR CAST(app_id AS CHAR) LIKE :search OR email LIKE :search)';
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['status'])) {
$where[] = 'company_status = :companyStatus';
$params['companyStatus'] = $filters['status'];
}
$sql = 'SELECT COUNT(*) FROM company_group';
if (!empty($where)) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
return (int)$conn->fetchOne($sql, $params);
}
private function getCompanyListSummary()
{
$conn = $this->getDoctrine()->getManager('company_group')->getConnection();
return array(
'all' => (int)$conn->fetchOne('SELECT COUNT(*) FROM company_group'),
'active' => (int)$conn->fetchOne("SELECT COUNT(*) FROM company_group WHERE company_status = 'active'"),
'trial' => (int)$conn->fetchOne("SELECT COUNT(*) FROM company_group WHERE company_status = 'trial'"),
'suspended' => (int)$conn->fetchOne("SELECT COUNT(*) FROM company_group WHERE company_status = 'suspended'"),
'expired' => (int)$conn->fetchOne("SELECT COUNT(*) FROM company_group WHERE company_status = 'expired'"),
'enabled' => (int)$conn->fetchOne('SELECT COUNT(*) FROM company_group WHERE active = 1'),
'disabled' => (int)$conn->fetchOne('SELECT COUNT(*) FROM company_group WHERE active = 0'),
);
}
public function companySettingsAction(Request $request, $appId)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_') {
return $this->redirectToRoute('dashboard');
}
if (!$this->canAccessSuperAdminDashboard($request)) {
return $this->redirectToRoute('dashboard');
}
$appId = (int)$appId;
$em = $this->getDoctrine()->getManager('company_group');
$company = $em->getRepository('CompanyGroupBundle\\Entity\\CompanyGroup')->findOneBy(array(
'appId' => $appId,
));
if (!$company) {
throw $this->createNotFoundException('Company appId #' . $appId . ' not found.');
}
if ($request->isMethod('POST')) {
$company->setName($request->request->get('name', $company->getName()));
$company->setAddress($request->request->get('address', $company->getAddress()));
$company->setShippingAddress($request->request->get('shippingAddress', $company->getShippingAddress()));
$company->setBillingAddress($request->request->get('billingAddress', $company->getBillingAddress()));
$company->setMotto($request->request->get('motto', $company->getMotto()));
$company->setInvoiceFooter($request->request->get('invoiceFooter', $company->getInvoiceFooter()));
$company->setGeneralFooter($request->request->get('generalFooter', $company->getGeneralFooter()));
$company->setCompanyDescription($request->request->get('companyDescription', $company->getCompanyDescription()));
$company->setCompanyStatus($request->request->get('companyStatus', $company->getCompanyStatus()));
$company->setPackageType($request->request->get('packageType', $company->getPackageType()));
$company->setActive((int)$request->request->get('active', 0));
$company->setReadOnlyMode((int)$request->request->get('readOnlyMode', 0));
$company->setAdminUserAllowed((int)$request->request->get('adminUserAllowed', 0));
$company->setUserAllowed((int)$request->request->get('userAllowed', 0));
$company->setSubscriptionMonth((int)$request->request->get('subscriptionMonth', 0));
$company->setCurrentSubscriptionPackageId((int)$request->request->get('currentSubscriptionPackageId', 0));
$company->setBillingAmount((int)$request->request->get('billingAmount', 0));
$usageValidUptoDate = $this->dateFromForm($request->request->get('usageValidUptoDate', ''));
$company->setUsageValidUptoDate($usageValidUptoDate);
$company->setUsageValidUptoDateTs($usageValidUptoDate ? $usageValidUptoDate->format('U') : 0);
$subscriptionExpiry = $this->dateFromForm($request->request->get('subscriptionExpiry', ''));
$company->setSubscriptionExpiry($subscriptionExpiry);
$moduleIds = $request->request->get('moduleIds', array());
if (!is_array($moduleIds)) {
$moduleIds = array();
}
$validModuleIds = array();
foreach (ModuleConstant::$moduleList as $module) {
$validModuleIds[(int)$module['id']] = true;
}
$enabledModuleIds = array();
foreach ($moduleIds as $moduleId) {
$moduleId = (int)$moduleId;
if ($moduleId > 0 && isset($validModuleIds[$moduleId])) {
$enabledModuleIds[$moduleId] = $moduleId;
}
}
ksort($enabledModuleIds);
$company->setEnabledModuleIdList(implode(',', array_values($enabledModuleIds)));
$em->flush();
$companySyncResult = $this->syncCompanySettingsToErp($em, $company);
$syncResult = $this->forceCompanyRouteSync($company);
if ($companySyncResult['success'] && $syncResult['success']) {
$this->addFlash('success', 'Company settings were saved and synced to ERP.');
} else {
$this->addFlash('warning', 'Company settings were saved, but ERP sync needs attention. Company sync: ' . $companySyncResult['message'] . ' Route sync: ' . $syncResult['message']);
}
return $this->redirectToRoute('admin_company_settings', array(
'appId' => $appId,
));
}
$enabledModuleIds = $this->parseCompanyModuleIdList($company->getEnabledModuleIdList());
if (empty($enabledModuleIds)) {
$enabledModuleIds = $this->getDefaultEnabledCompanyModuleIds();
}
$enabledLookup = array_fill_keys($enabledModuleIds, true);
$groupedModules = $this->buildGroupedModuleList();
return $this->render('@CompanyGroup/pages/admin/companies/module_settings.html.twig', array(
'page_title' => 'Company Settings',
'company' => $company,
'grouped_modules' => $groupedModules,
'enabled_lookup' => $enabledLookup,
'enabled_count' => count($enabledLookup),
'module_count' => count(ModuleConstant::$moduleList),
));
}
public function companyModuleSettingsAction(Request $request, $appId)
{
return $this->companySettingsAction($request, $appId);
}
private function dateFromForm($value)
{
$value = trim((string)$value);
if ($value === '') {
return null;
}
try {
return new \DateTime($value);
} catch (\Exception $e) {
return null;
}
}
private function syncCompanySettingsToErp($em, $company)
{
$response = MiscActions::updateCompanyToErpServer($em, (int)$company->getAppId(), $this->container->getParameter('kernel.root_dir'));
if (isset($response['success']) && $response['success'] === true) {
return array(
'success' => true,
'message' => isset($response['message']) ? $response['message'] : 'Synced.',
);
}
return array(
'success' => false,
'message' => isset($response['message']) ? $response['message'] : 'Company metadata sync was not confirmed.',
);
}
private function parseCompanyModuleIdList($moduleIdList)
{
$moduleIdList = trim((string)$moduleIdList);
if ($moduleIdList === '') {
return array();
}
$decoded = json_decode($moduleIdList, true);
$rawList = is_array($decoded) ? $decoded : explode(',', $moduleIdList);
$cleanList = array();
foreach ($rawList as $moduleId) {
$moduleId = (int)$moduleId;
if ($moduleId > 0) {
$cleanList[$moduleId] = $moduleId;
}
}
return array_values($cleanList);
}
private function getDefaultEnabledCompanyModuleIds()
{
$moduleIds = array();
foreach (ModuleConstant::$moduleList as $module) {
if ((int)(isset($module['defaultEnabledForCompany']) ? $module['defaultEnabledForCompany'] : 0) === 1) {
$moduleIds[] = (int)$module['id'];
}
}
return $moduleIds;
}
private function buildGroupedModuleList()
{
$groups = array();
foreach (ModuleConstant::$parentModuleList as $parentModule) {
$groups[(int)$parentModule['id']] = array(
'parent' => $parentModule,
'modules' => array(),
);
}
foreach (ModuleConstant::$moduleList as $module) {
$parentId = (int)$module['parentId'];
if (!isset($groups[$parentId])) {
$groups[$parentId] = array(
'parent' => array(
'id' => $parentId,
'name' => 'Other',
),
'modules' => array(),
);
}
$groups[$parentId]['modules'][] = $module;
}
foreach ($groups as $parentId => $group) {
if (empty($group['modules'])) {
unset($groups[$parentId]);
}
}
return $groups;
}
private function forceCompanyRouteSync($company)
{
$serverAddress = rtrim((string)$company->getCompanyGroupServerAddress(), '/');
if ($serverAddress === '') {
return array(
'success' => false,
'message' => 'ERP server address is not configured.',
);
}
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_POST => 1,
CURLOPT_URL => $serverAddress . '/update_route_company_wise',
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_POSTFIELDS => http_build_query(array(
'appId' => (int)$company->getAppId(),
)),
));
$response = curl_exec($curl);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
return array(
'success' => false,
'message' => $error,
);
}
return array(
'success' => true,
'message' => (string)$response,
);
}
private function buildChartData(array $usageSummary)
{
$activityTrend = $usageSummary['activity_trend'] ?? [];
$usageTrend = $usageSummary['usage_trend'] ?? [];
$revenueTrend = $usageSummary['revenue_trend'] ?? [];
$activityByDay = [];
foreach ($activityTrend as $row) {
$day = (string) ($row['day'] ?? '');
if ($day === '') {
continue;
}
if (!isset($activityByDay[$day])) {
$activityByDay[$day] = 0;
}
$activityByDay[$day] += (int) ($row['total'] ?? 0);
}
$usageByDay = [];
foreach ($usageTrend as $row) {
$day = (string) ($row['day'] ?? '');
if ($day === '') {
continue;
}
if (!isset($usageByDay[$day])) {
$usageByDay[$day] = 0;
}
$usageByDay[$day] += (int) ($row['total'] ?? 0);
}
$revenueByDay = [];
foreach ($revenueTrend as $row) {
$day = (string) ($row['day'] ?? '');
if ($day === '') {
continue;
}
$revenueByDay[$day] = (float) ($row['total'] ?? 0);
}
$labels = array_values(array_unique(array_merge(
array_keys($activityByDay),
array_keys($usageByDay),
array_keys($revenueByDay)
)));
sort($labels);
$activitySeries = [];
$usageSeries = [];
$revenueSeries = [];
foreach ($labels as $label) {
$activitySeries[] = (int) ($activityByDay[$label] ?? 0);
$usageSeries[] = (int) ($usageByDay[$label] ?? 0);
$revenueSeries[] = (float) ($revenueByDay[$label] ?? 0);
}
return [
'labels' => $labels,
'activity_series' => $activitySeries,
'usage_series' => $usageSeries,
'revenue_series' => $revenueSeries,
];
}
// ── EB0-UI — Energy⇄Business Bridge monitor + site-link admin ────────────────────────────────
// "My sites ↔ bridge status" for the owner: watch events land, see Own vs Customer, and map an
// unlinked site to a tenant + customer without hand-typing ids. Read-only over the CENTRAL bridge
// tables (+ a tenant-scoped AccClients search for the customer picker). No business action here.
public function energyBridgeAction(Request $request)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_') { return $this->redirectToRoute('dashboard'); }
if (!$this->canAccessSuperAdminDashboard($request)) { return $this->redirectToRoute('dashboard'); }
$cem = $this->getDoctrine()->getManager('company_group');
// Tenant directory (appId → name) for display + the picker.
$tenants = array();
try {
foreach ($cem->getRepository('CompanyGroupBundle\\Entity\\CompanyGroup')->findBy(array('active' => 1)) as $g) {
if (!$g->getDbName()) { continue; }
$tenants[(int) $g->getAppId()] = $g->getName();
}
} catch (\Throwable $e) { /* central unreachable */ }
// Site-link map + recent events. Pre-schema-safe: if the tables aren't migrated yet, degrade
// to an empty view with a notice rather than 500 (same discipline as the fleet-health page).
$links = array(); $events = array(); $schemaReady = true;
try {
foreach ($cem->getRepository('CompanyGroupBundle\\Entity\\EnergyBridgeSiteLink')->findBy(array(), array('siteUid' => 'ASC')) as $l) {
$links[(string) $l->getSiteUid()] = array(
'siteUid' => $l->getSiteUid(), 'appId' => (int) $l->getAppId(),
'tenant' => isset($tenants[(int) $l->getAppId()]) ? $tenants[(int) $l->getAppId()] : ('app ' . $l->getAppId()),
'linkType' => $l->getLinkType() ?: 'self', 'customerId' => $l->getCustomerId(),
'projectId' => $l->getProjectId(), 'note' => $l->getNote(),
);
}
$qb = $cem->getRepository('CompanyGroupBundle\\Entity\\EnergyBridgeEvent')
->createQueryBuilder('e')->orderBy('e.id', 'DESC')->setMaxResults(60);
foreach ($qb->getQuery()->getResult() as $e) {
$events[] = array(
'eventId' => $e->getEventId(), 'eventType' => $e->getEventType(),
'siteUid' => $e->getSiteUid(), 'appId' => (int) $e->getAppId(),
'tenant' => $e->getAppId() && isset($tenants[(int) $e->getAppId()]) ? $tenants[(int) $e->getAppId()] : null,
'linkType' => $e->getLinkType(), 'status' => $e->getStatus(),
'receivedAt' => $e->getReceivedAt(), 'correlationId' => $e->getCorrelationId(),
);
}
} catch (\Throwable $e) { $schemaReady = false; }
// KPIs.
$today = (new \DateTime())->format('Y-m-d');
$kpi = array('total' => count($events), 'unlinked' => 0, 'pending' => 0, 'today' => 0, 'lastReceived' => null, 'linkedSites' => count($links));
$unlinkedSites = array();
foreach ($events as $e) {
if ($e['status'] === 'unlinked') {
$kpi['unlinked']++;
if ($e['siteUid'] !== null && $e['siteUid'] !== '' && !isset($links[(string) $e['siteUid']])) {
$unlinkedSites[(string) $e['siteUid']] = true;
}
}
if ($e['status'] === 'pending') { $kpi['pending']++; }
if ($e['receivedAt'] instanceof \DateTime) {
if ($kpi['lastReceived'] === null || $e['receivedAt'] > $kpi['lastReceived']) { $kpi['lastReceived'] = $e['receivedAt']; }
if ($e['receivedAt']->format('Y-m-d') === $today) { $kpi['today']++; }
}
}
return $this->render('@CompanyGroup/pages/admin/energy_bridge.html.twig', array(
'configured' => EbBridgeConfig::isConfigured(),
'schemaReady' => $schemaReady,
'tenants' => $tenants,
'links' => array_values($links),
'events' => $events,
'kpi' => $kpi,
'unlinkedSites' => array_keys($unlinkedSites),
));
}
/** Create/update a site-link from the admin form (mirrors inno:energy-site-link). */
public function energyBridgeLinkAction(Request $request)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_') { return $this->redirectToRoute('dashboard'); }
if (!$this->canAccessSuperAdminDashboard($request)) { return $this->redirectToRoute('dashboard'); }
$siteUid = trim((string) $request->request->get('siteUid', ''));
$appId = (int) $request->request->get('appId', 0);
$customerId = (int) $request->request->get('customerId', 0);
$projectId = (int) $request->request->get('projectId', 0);
$note = trim((string) $request->request->get('note', ''));
if ($siteUid === '' || $appId <= 0) {
$this->addFlash('error', 'Site UID and tenant are required to map a site.');
return $this->redirectToRoute('energy_bridge_monitor');
}
try {
$cem = $this->getDoctrine()->getManager('company_group');
$link = $cem->getRepository('CompanyGroupBundle\\Entity\\EnergyBridgeSiteLink')->findOneBy(array('siteUid' => $siteUid));
$now = new \DateTime();
if ($link === null) {
$link = new EnergyBridgeSiteLink();
$link->setSiteUid($siteUid);
$link->setCreatedAt($now);
}
$link->setAppId($appId);
$link->setCustomerId($customerId > 0 ? $customerId : null);
$link->setProjectId($projectId > 0 ? $projectId : null);
$link->setNote($note !== '' ? $note : null);
// A customer chosen → this is a client PPA site; none → our own site.
$link->setLinkType(EbBridgeVerifier::linkTypeForCustomer($customerId));
$link->setUpdatedAt($now);
$cem->persist($link);
$cem->flush();
$this->addFlash('success', sprintf('Mapped site %s → app %d (%s).', $siteUid, $appId,
$link->getLinkType() === 'customer' ? 'customer' : 'own site'));
} catch (\Throwable $e) {
$this->addFlash('error', 'Could not save the site link (is the central schema migrated?): ' . $e->getMessage());
}
return $this->redirectToRoute('energy_bridge_monitor');
}
/**
* AJAX: search a specific tenant's customers (AccClients) so the owner never hand-types a
* customer id. Switches the default connection to that tenant for this request only.
*/
public function energyBridgeCustomerSearchAction(Request $request)
{
$systemType = $this->container->hasParameter('system_type') ? $this->container->getParameter('system_type') : '_ERP_';
if ($systemType !== '_CENTRAL_' || !$this->canAccessSuperAdminDashboard($request)) {
return new JsonResponse(array('results' => array(), 'error' => 'forbidden'), 403);
}
$appId = (int) $request->query->get('appId', 0);
$q = trim((string) $request->query->get('q', ''));
if ($appId <= 0) { return new JsonResponse(array('results' => array())); }
try {
$cfg = $this->get('app.tenant_db_config_resolver')->resolveByAppId($appId);
$this->get('application_connector')->resetConnection('default', $cfg['dbName'], $cfg['dbUser'], $cfg['dbPassword'], $cfg['dbHost'], true);
$conn = $this->getDoctrine()->getManager()->getConnection();
$rows = $conn->fetchAll(
'SELECT client_id, client_name FROM acc_clients WHERE client_name LIKE :q ORDER BY client_name LIMIT 20',
array('q' => '%' . $q . '%'));
$results = array();
foreach ($rows as $r) {
$results[] = array('id' => (int) $r['client_id'], 'name' => (string) $r['client_name']);
}
return new JsonResponse(array('results' => $results));
} catch (\Throwable $e) {
return new JsonResponse(array('results' => array(), 'error' => 'tenant unreachable'));
}
}
}