<?php
namespace App\Controller\Backend;
use App\Controller\Base\BaseController;
use App\Entity\DashboardComponent;
use App\Entity\DashboardTab;
use App\Entity\TGrafik;
use App\Repository\DashboardComponentRepository;
use App\Repository\DashboardTabRepository;
use App\Repository\TExploreDataDetailRepository;
use App\Repository\TGrafikRepository;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\DBAL\Connection;
class DashboardComponentController extends BaseController
{
/**
* @Route("/dashboard_component/modal/{id}", name="dashboard_component_modal", methods={"GET"})
*/
public function showAddModal(DashboardTab $tab, TExploreDataDetailRepository $tExploreDataDetailRepository, TGrafikRepository $tGrafikRepository, $id): Response
{
$dataSources = $tExploreDataDetailRepository->findAll();
$viewDataSources = $tGrafikRepository->findAll();
$combinedDataSources = [];
foreach ($dataSources as $item) {
$combinedDataSources[] = [
'id' => $item->getId(),
'label' => $item->getNama(),
'type' => 't_explore_data_detail'
];
}
foreach ($viewDataSources as $item) {
$combinedDataSources[] = [
'id' => $item->getId(),
'label' => $item->getJudul(),
'type' => 't_grafik',
];
}
return $this->render('backend/dashboard_component/component_modal.html.twig', [
'tab' => $tab,
'tab_id' => $id,
'dataSources' => $dataSources,
'viewDataSources' => $viewDataSources,
'combinedDataSources' => $combinedDataSources
]);
}
/**
* @Route("/dashboard_component/edit_card_modal/{id}", name="edit_card_modal", methods={"GET"})
*/
public function showEditCardModal(DashboardComponent $component): Response
{
return $this->render('backend/dashboard_component/edit_card_modal.html.twig', [
'component' => $component
]);
}
/**
* @Route("/dashboard_component/add", name="dashboard_component_add", methods={"POST"})
*/
public function addComponent(
Request $request,
EntityManagerInterface $em,
DashboardTabRepository $tabRepo,
TGrafikRepository $tGrafikRepository
): JsonResponse {
$tabId = $request->request->get('tab_id');
$tab = $tabRepo->find($tabId);
if (!$tab) {
return new JsonResponse(['success' => false, 'message' => 'Tab tidak ditemukan'], 404);
}
$component = new DashboardComponent();
$component->setDashboardTab($tab);
$component->setTitle($request->request->get('title'));
$component->setType($request->request->get('type'));
// Styling
$component->setTextColor($request->request->get('text_color'));
$component->setBackgroundColor($request->request->get('background_color'));
$component->setWidth((int) $request->request->get('width', 6));
$component->setHeight((int) $request->request->get('height', 4));
// Inisialisasi config
$config = [];
$type = $component->getType();
if ($type === 'chart') {
$externalChartId = $request->request->get('external_chart_id');
$chartType = $request->request->get('chart_type');
$sourceType = $request->request->get('chart_source_type');
$config = [
'external_chart_id' => $externalChartId,
'chart_type' => $chartType,
'data_source_type' => $sourceType,
];
$component->setChartType($chartType);
$component->setExternalChartId($externalChartId);
$component->setSourceType($sourceType);
} elseif ($type === 'table') {
$dataSource = $request->request->get('data_source_table');
$tableStyle = $request->request->get('table_style');
$externalChartId = $request->request->get('data_source_table');
$sourceType = $request->request->get('table_source_type');
$config = [
'data_source' => $dataSource,
'data_source_type' => $sourceType,
'style' => $tableStyle,
'external_chart_id' => $externalChartId
];
$component->setDataSource($dataSource);
$component->setExternalChartId($externalChartId);
$component->setSourceType($sourceType);
} elseif ($type === 'card') {
$dataSource = $request->request->get('data_source_card');
$field = $request->request->get('card_field');
$sourceType = $request->request->get('card_source_type');
$config = [
'data_source' => $dataSource,
'data_source_type' => $sourceType,
'card_field' => $field
];
// Reuse positionY untuk menyimpan kolom field
$component->setDataSource($dataSource);
$component->setSourceType($sourceType);
} elseif ($type === 'html') {
$grafikId = $request->request->get('data_source_html');
$htmlTemplate = html_entity_decode($request->request->get('html_template'));
$sourceType = $request->request->get('html_source_type');
$component->setSourceType($sourceType);
$component->setDataSource($grafikId); // penting agar bisa dipakai saat preview
$config = [
'data_source' => $grafikId,
'data_source_type' => $sourceType,
'html_template' => $htmlTemplate,
];
}
// Simpan config JSON
$component->setConfig($config);
// Persist
$em->persist($component);
$em->flush();
return new JsonResponse(['success' => true, 'id' => $component->getId()]);
}
/**
* @Route("/dashboard_component/update_position", name="dashboard_component_update_position", methods={"POST"})
*/
public function updatePosition(
Request $request,
DashboardComponentRepository $repo,
EntityManagerInterface $em
): JsonResponse {
$data = json_decode($request->getContent(), true);
if (!isset($data['components'])) {
return new JsonResponse(['success' => false, 'error' => 'Invalid payload'], 400);
}
foreach ($data['components'] as $item) {
if (!isset($item['id'])) continue;
$component = $repo->find($item['id']);
if ($component) {
if (isset($item['width'])) $component->setWidth($item['width']);
if (isset($item['height'])) $component->setHeight($item['height']);
if (isset($item['x'])) $component->setPositionX($item['x']);
if (isset($item['y'])) $component->setPositionY($item['y']);
$em->persist($component);
}
}
$em->flush();
return new JsonResponse(['success' => true]);
}
/**
* @Route("/dashboard_component/update", name="dashboard_component_update", methods={"POST"})
*/
public function update(Request $request, EntityManagerInterface $em, DashboardComponentRepository $repo): JsonResponse
{
$id = $request->request->get('id');
$component = $repo->find($id);
if (!$component) {
return new JsonResponse(['success' => false], 404);
}
$component->setTitle($request->request->get('title'));
$component->setTextColor($request->request->get('text_color'));
$component->setBackgroundColor($request->request->get('background_color'));
$component->setWidth((int) $request->request->get('width'));
$component->setHeight((int) $request->request->get('height'));
$em->flush();
return new JsonResponse(['success' => true]);
}
/**
* @Route("/dashboard_component/delete/{id}", name="dashboard_component_delete", methods={"POST"})
*/
public function deleteComponent(DashboardComponent $component, EntityManagerInterface $em): JsonResponse
{
try {
$em->remove($component);
$em->flush();
return new JsonResponse(['success' => true]);
} catch (\Exception $e) {
return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* @Route("/dashboard_component/table/{id}/embed", name="dashboard_component_embed_table", methods={"GET"})
*/
public function embedTable(
int $id,
DashboardComponentRepository $repo,
TGrafikRepository $grafikRepo
): Response {
$component = $repo->find($id);
$grafikId = $component->getConfig()['external_chart_id'] ?? null;
$grafik = $grafikRepo->find($grafikId);
$columns = $this->getGrafikColumns($grafik);
return $this->render('backend/t_grafik/_embed_table.html.twig', [
'grafik' => $grafik,
'columns' => $columns,
]);
}
private function getGrafikColumns(TGrafik $grafik): array
{
$sumbu_x = ($grafik->getAxisx() === 'kategori_plant') ? 'plant' : $grafik->getAxisx();
$sumbu_y = $grafik->getAxisYIds();
if ($grafik->getAxisx() == 'all') {
return $sumbu_y; // jika axis_x adalah 'all', hanya kembalikan sumbu_y
}
return array_merge([$sumbu_x], $sumbu_y);
}
/**
* @Route("/dashboard_component/data-source/preview-fields/{id}", name="dashboard_component_preview_fields", methods={"GET"})
*/
public function previewFields(
int $id,
TGrafikRepository $repo,
EntityManagerInterface $em
): JsonResponse {
$grafik = $repo->find($id);
if (!$grafik || !$grafik->getTabel()) {
return new JsonResponse(['success' => false, 'message' => 'Data source tidak valid']);
}
$table = $grafik->getTabel();
$sql = 'SELECT * FROM "' . str_replace('"', '', $table) . '" LIMIT 1';
try {
$conn = $em->getConnection();
$stmt = $conn->prepare($sql);
$result = $stmt->executeQuery();
$row = $result->fetchAssociative();
// Cek field valid
$fields = $row ? array_keys($row) : [];
return new JsonResponse([
'success' => true,
'fields' => $fields // hanya kunci yang benar-benar ada
]);
} catch (\Throwable $e) {
return new JsonResponse([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// /**
// * @Route("/dashboard_component/embed_table_by_data_source/{id}", name="dashboard_component_embed_table_by_data_source", methods={"GET"})
// */
// public function embedTableByDataSource(
// int $id,
// TGrafikRepository $grafikRepo,
// EntityManagerInterface $em
// ): Response {
// $grafik = $grafikRepo->find($id);
// if (!$grafik) {
// throw $this->createNotFoundException('Data source tidak ditemukan');
// }
// $columns = $this->getGrafikColumns($grafik);
// // Ambil data dari SQL
// $conn = $em->getConnection();
// $table = $grafik->getTabel();
// $axisX = $grafik->getAxisx() === 'kategori_plant' ? 'plant' : $grafik->getAxisx();
// $axisY = $grafik->getAxisYIds(); // array
// $operation = $grafik->getOperation() ?: 'COUNT';
// $selectParts = [];
// foreach ($axisY as $col) {
// $selectParts[] = "$operation(\"$col\") AS \"$col\"";
// }
// if ($grafik->getAxisx() == 'all') {
// $selectClause = implode(", ", $selectParts);
// $sql = "SELECT $selectClause FROM \"$table\" LIMIT 10";
// } else {
// $selectClause = "\"$axisX\", " . implode(", ", $selectParts);
// $sql = "SELECT $selectClause FROM \"$table\" GROUP BY \"$axisX\" ORDER BY \"$axisX\" LIMIT 10";
// }
// try {
// $stmt = $conn->prepare($sql);
// $rows = $stmt->executeQuery()->fetchAllAssociative();
// } catch (\Throwable $e) {
// $rows = [];
// }
// return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
// 'grafik' => $grafik,
// 'columns' => $columns,
// 'data' => $rows
// ]);
// }
/**
* @Route("/dashboard_component/embed_table_by_data_source/{id}", name="dashboard_component_embed_table_by_data_source", methods={"GET"})
*/
public function embedTableByDataSource(
int $id,
Request $request,
TGrafikRepository $grafikRepo,
TExploreDataDetailRepository $exploreRepo,
EntityManagerInterface $em
): Response {
$type = $request->query->get('type');
// ----------------------------------------------------------------------
// Sumber: Analytics / t_explore_data_detail -> SUM(nilai) per periode
// ----------------------------------------------------------------------
if ($type === 't_explore_data_detail') {
// boleh lewat param ?explorasi=... atau fallback ke {id}
$exploreId = $request->query->get('explorasi', $id);
$explore = $exploreRepo->find($exploreId);
if (!$explore) {
return new Response("Data source Analytics tidak ditemukan", 404);
}
// tentukan sumber: view atau tabel
$tableOrView = $explore->isIsCreateView() ? $explore->getView() : $explore->getTabel();
if (!$tableOrView) {
return new Response("View/Tabel untuk Analytics belum diset", 400);
}
$ident = preg_replace('/[^A-Za-z0-9_\.]/', '', $tableOrView);
$parts = explode('.', $ident);
$qname = implode('.', array_map(fn($p) => '"' . str_replace('"', '""', $p) . '"', $parts));
$conn = $em->getConnection();
$where = ["COALESCE(t.periode,'') <> ''"];
$filtersFromExplore = $explore->getFilter();
if (!$explore->isIsCreateView() && is_array($filtersFromExplore) && !empty($filtersFromExplore)) {
$where[] = '(' . implode(' AND ', $filtersFromExplore) . ')';
}
$uiFilters = $request->query->all('filters') ?? [];
$params = [];
if (!empty($uiFilters['periode'])) {
$where[] = "t.periode ILIKE :periode";
$params['periode'] = '%' . $uiFilters['periode'] . '%';
}
if (!empty($uiFilters['nilai'])) {
$where[] = "t.nilai::text ILIKE :nilai";
$params['nilai'] = '%' . $uiFilters['nilai'] . '%';
}
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$orderSql = "
ORDER BY
TO_DATE(LEFT(t.periode, 3) || ' ' || SUBSTRING(t.periode, 4, 4), 'Mon YYYY') ASC,
CASE SPLIT_PART(t.periode, '.', 2)
WHEN 'I' THEN 1
WHEN 'II' THEN 2
WHEN 'III' THEN 3
WHEN 'IV' THEN 4
ELSE 0
END ASC
";
// preview: 10 baris
$limit = 10;
$offset = 0;
$sql = "
SELECT
t.periode,
SUM(t.nilai) AS nilai
FROM {$qname} t
{$whereSql}
GROUP BY t.periode
{$orderSql}
LIMIT {$limit} OFFSET {$offset}
";
try {
$rows = $conn->executeQuery($sql, $params)->fetchAllAssociative();
} catch (\Throwable $e) {
$rows = [];
}
$columns = ['periode', 'nilai'];
return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
'columns' => $columns,
'data' => $rows,
]);
}
// ---------------------------------------
// Sumber: t_grafik (preview data source)
// ---------------------------------------
$grafik = $grafikRepo->find($id);
if (!$grafik) {
return new Response("Data grafik tidak ditemukan", 404);
}
$conn = $em->getConnection();
$table = $grafik->getTabel();
$axisXConf = $grafik->getAxisx();
$axisX = $axisXConf === 'kategori_plant' ? 'plant' : $axisXConf;
$axisY = $grafik->getAxisYIds() ?: [];
$operation = strtoupper(trim($grafik->getOperation() ?: 'COUNT'));
$quoteIdent = function (?string $ident): string {
$ident = preg_replace('/[^A-Za-z0-9_\.]/', '', $ident ?? '');
$parts = array_filter(explode('.', $ident), fn($p) => $p !== '');
return implode('.', array_map(fn($p) => '"' . str_replace('"', '""', $p) . '"', $parts));
};
$qTable = $quoteIdent($table);
$buildAgg = function (string $op, string $col) use ($quoteIdent) {
$qCol = $quoteIdent($col);
switch ($op) {
case 'COUNT_DISTINCT':
return sprintf('COUNT(DISTINCT %s) AS "%s"', $qCol, str_replace('"', '""', $col));
case 'SUM':
case 'AVG':
case 'MIN':
case 'MAX':
case 'COUNT':
default:
return sprintf('%s(%s) AS "%s"', $op, $qCol, str_replace('"', '""', $col));
}
};
// =========================
// axisx = 'all'
// =========================
if ($axisXConf === 'all') {
// Seleksi hanya agregat per kolom Y -> 1 baris
$selectParts = [];
if (empty($axisY)) {
$selectParts[] = 'COUNT(*) AS total';
$columns = ['total'];
} else {
foreach ($axisY as $col) {
$selectParts[] = $buildAgg($operation, $col);
}
$columns = $axisY; // header mengikuti nama kolom axisY
}
$sql = sprintf('SELECT %s FROM %s', implode(', ', $selectParts), $qTable);
try {
$rows = $conn->executeQuery($sql)->fetchAllAssociative();
} catch (\Throwable $e) {
$rows = [];
}
return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
'columns' => $columns,
'data' => $rows,
]);
}
// =========================
// axisx ≠ 'all'
// =========================
$selectParts = [];
if (empty($axisY)) {
$selectParts[] = 'COUNT(*) AS total';
$columns = [$axisX, 'total'];
} else {
foreach ($axisY as $col) {
$selectParts[] = $buildAgg($operation, $col);
}
$columns = array_merge([$axisX], $axisY);
}
$qAxisX = $quoteIdent($axisX);
$sql = sprintf(
'SELECT %s, %s FROM %s GROUP BY %s ORDER BY %s LIMIT 10',
$qAxisX,
implode(', ', $selectParts),
$qTable,
$qAxisX,
$qAxisX
);
try {
$rows = $conn->executeQuery($sql)->fetchAllAssociative();
} catch (\Throwable $e) {
$rows = [];
}
return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
'columns' => $columns,
'data' => $rows,
]);
}
/**
* @Route("/dashboard_component/{id}/get_html_json", name="dashboard_component_get_html_json", methods={"GET"})
*/
public function getHtmlJson(
int $id,
DashboardComponentRepository $repo,
TGrafikRepository $tGrafikRepo,
EntityManagerInterface $em
): JsonResponse {
$component = $repo->find($id);
$grafikId = $component->getDataSource();
$tGrafik = $tGrafikRepo->find($grafikId);
if (!$tGrafik) {
return new JsonResponse([]);
}
$conn = $em->getConnection();
$table = $tGrafik->getTabel();
$axisX = $tGrafik->getAxisx() === 'kategori_plant' ? 'plant' : $tGrafik->getAxisx();
$axisY = $tGrafik->getAxisYIds(); // array
$operation = $tGrafik->getOperation() ?: 'COUNT';
$columns = array_merge([$axisX], $axisY);
$selectParts = [];
foreach ($axisY as $col) {
if ($tGrafik->getOperation() == 'COUNT_DISTINCT') {
$selectParts[] = "COUNT(DISTINCT(\"$col\")) AS \"$col\"";
} else {
$selectParts[] = "$operation(\"$col\") AS \"$col\"";
}
}
if ($tGrafik->getAxisx() == 'all') {
$selectClause = implode(", ", $selectParts);
$sql = "SELECT $selectClause FROM \"$table\" LIMIT 1";
} else {
$selectClause = "\"$axisX\", " . implode(", ", $selectParts);
$sql = "SELECT $selectClause FROM \"$table\" GROUP BY \"$axisX\" ORDER BY \"$axisX\" LIMIT 1";
}
try {
$stmt = $conn->prepare($sql);
$data = $stmt->executeQuery()->fetchAllAssociative();
} catch (\Throwable $e) {
$data = [];
}
return new JsonResponse($data);
}
/**
* @Route("/dashboard_component/explore/chart-data/{id}", name="dashboard_component_chart_data_explore", methods={"GET"})
*/
public function getExploreChartData(
int $id,
DashboardComponentRepository $repo,
TExploreDataDetailRepository $exploreRepo,
Request $request
): JsonResponse {
$comp = $repo->find($id);
$config = $comp->getConfig();
$exploreId = $config['external_chart_id'] ?? null;
$xAxis = $config['x_axis'] ?? 'periode';
$yAxis = $config['y_axis'] ?? 'nilai';
$chartType = $config['chart_type'] ?? 'column';
$explore = $exploreRepo->find($exploreId);
if (!$explore) {
return $this->json(['error' => 'Sumber data eksplorasi tidak ditemukan'], 404);
}
// Ambil nama tabel atau view
$table = $explore->isIsCreateView() ? $explore->getView() : $explore->getTabel();
// Kirim semua data
$filters = []; // kamu bisa isi dari GET kalau mau, tapi kosongkan jika chart tidak pakai filter
$data = $this->getDataTableDetailExplorasiPeriode(null, null, $filters, $table, $exploreId);
if (empty($data)) {
return $this->json(['error' => 'Data eksplorasi kosong'], 400);
}
if (!isset($data[0][$xAxis]) || !isset($data[0][$yAxis])) {
return $this->json(['error' => "Kolom '$xAxis' atau '$yAxis' tidak ditemukan"], 400);
}
$categories = array_column($data, $xAxis);
$series = array_map(fn($row) => (float) $row[$yAxis], $data);
return $this->json([
'chart_type' => $chartType,
'x_axis' => $xAxis,
'y_axis' => $yAxis,
'columns' => $categories,
'datas' => [[
'name' => $yAxis,
'data' => $series
]]
]);
}
}