src/Controller/Backend/DashboardComponentController.php line 59

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Backend;
  3. use App\Controller\Base\BaseController;
  4. use App\Entity\DashboardComponent;
  5. use App\Entity\DashboardTab;
  6. use App\Entity\TGrafik;
  7. use App\Repository\DashboardComponentRepository;
  8. use App\Repository\DashboardTabRepository;
  9. use App\Repository\TExploreDataDetailRepository;
  10. use App\Repository\TGrafikRepository;
  11. use Symfony\Component\Routing\Annotation\Route;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\JsonResponse;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Doctrine\DBAL\Connection;
  17. class DashboardComponentController extends BaseController
  18. {
  19.     /**
  20.      * @Route("/dashboard_component/modal/{id}", name="dashboard_component_modal", methods={"GET"})
  21.      */
  22.     public function showAddModal(DashboardTab $tabTExploreDataDetailRepository $tExploreDataDetailRepositoryTGrafikRepository $tGrafikRepository$id): Response
  23.     {
  24.         $dataSources $tExploreDataDetailRepository->findAll();
  25.         $viewDataSources $tGrafikRepository->findAll();
  26.         $combinedDataSources = [];
  27.         foreach ($dataSources as $item) {
  28.             $combinedDataSources[] = [
  29.                 'id' => $item->getId(),
  30.                 'label' => $item->getNama(),
  31.                 'type' => 't_explore_data_detail'
  32.             ];
  33.         }
  34.         foreach ($viewDataSources as $item) {
  35.             $combinedDataSources[] = [
  36.                 'id' => $item->getId(),
  37.                 'label' => $item->getJudul(),
  38.                 'type' => 't_grafik',
  39.             ];
  40.         }
  41.         return $this->render('backend/dashboard_component/component_modal.html.twig', [
  42.             'tab' => $tab,
  43.             'tab_id' => $id,
  44.             'dataSources' => $dataSources,
  45.             'viewDataSources' => $viewDataSources,
  46.             'combinedDataSources' => $combinedDataSources
  47.         ]);
  48.     }
  49.     /**
  50.      * @Route("/dashboard_component/edit_card_modal/{id}", name="edit_card_modal", methods={"GET"})
  51.      */
  52.     public function showEditCardModal(DashboardComponent $component): Response
  53.     {
  54.         return $this->render('backend/dashboard_component/edit_card_modal.html.twig', [
  55.             'component' => $component
  56.         ]);
  57.     }
  58.     /**
  59.      * @Route("/dashboard_component/add", name="dashboard_component_add", methods={"POST"})
  60.      */
  61.     public function addComponent(
  62.         Request $request,
  63.         EntityManagerInterface $em,
  64.         DashboardTabRepository $tabRepo,
  65.         TGrafikRepository $tGrafikRepository
  66.     ): JsonResponse {
  67.         $tabId $request->request->get('tab_id');
  68.         $tab $tabRepo->find($tabId);
  69.         if (!$tab) {
  70.             return new JsonResponse(['success' => false'message' => 'Tab tidak ditemukan'], 404);
  71.         }
  72.         $component = new DashboardComponent();
  73.         $component->setDashboardTab($tab);
  74.         $component->setTitle($request->request->get('title'));
  75.         $component->setType($request->request->get('type'));
  76.         // Styling
  77.         $component->setTextColor($request->request->get('text_color'));
  78.         $component->setBackgroundColor($request->request->get('background_color'));
  79.         $component->setWidth((int) $request->request->get('width'6));
  80.         $component->setHeight((int) $request->request->get('height'4));
  81.         // Inisialisasi config
  82.         $config = [];
  83.         $type $component->getType();
  84.         if ($type === 'chart') {
  85.             $externalChartId $request->request->get('external_chart_id');
  86.             $chartType $request->request->get('chart_type');
  87.             $sourceType $request->request->get('chart_source_type');
  88.             $config = [
  89.                 'external_chart_id' => $externalChartId,
  90.                 'chart_type' => $chartType,
  91.                 'data_source_type' => $sourceType,
  92.             ];
  93.             $component->setChartType($chartType);
  94.             $component->setExternalChartId($externalChartId);
  95.             $component->setSourceType($sourceType);
  96.         } elseif ($type === 'table') {
  97.             $dataSource $request->request->get('data_source_table');
  98.             $tableStyle $request->request->get('table_style');
  99.             $externalChartId $request->request->get('data_source_table');
  100.             $sourceType $request->request->get('table_source_type');
  101.             $config = [
  102.                 'data_source' => $dataSource,
  103.                 'data_source_type' => $sourceType,
  104.                 'style' => $tableStyle,
  105.                 'external_chart_id' => $externalChartId
  106.             ];
  107.             $component->setDataSource($dataSource);
  108.             $component->setExternalChartId($externalChartId);
  109.             $component->setSourceType($sourceType);
  110.         } elseif ($type === 'card') {
  111.             $dataSource $request->request->get('data_source_card');
  112.             $field $request->request->get('card_field');
  113.             $sourceType $request->request->get('card_source_type');
  114.             $config = [
  115.                 'data_source' => $dataSource,
  116.                 'data_source_type' => $sourceType,
  117.                 'card_field' => $field
  118.             ];
  119.             // Reuse positionY untuk menyimpan kolom field
  120.             $component->setDataSource($dataSource);
  121.             $component->setSourceType($sourceType);
  122.         } elseif ($type === 'html') {
  123.             $grafikId $request->request->get('data_source_html');
  124.             $htmlTemplate html_entity_decode($request->request->get('html_template'));
  125.             $sourceType $request->request->get('html_source_type');
  126.             $component->setSourceType($sourceType);
  127.             $component->setDataSource($grafikId); // penting agar bisa dipakai saat preview
  128.             $config = [
  129.                 'data_source' => $grafikId,
  130.                 'data_source_type' => $sourceType,
  131.                 'html_template' => $htmlTemplate,
  132.             ];
  133.         }
  134.         // Simpan config JSON
  135.         $component->setConfig($config);
  136.         // Persist
  137.         $em->persist($component);
  138.         $em->flush();
  139.         return new JsonResponse(['success' => true'id' => $component->getId()]);
  140.     }
  141.     /**
  142.      * @Route("/dashboard_component/update_position", name="dashboard_component_update_position", methods={"POST"})
  143.      */
  144.     public function updatePosition(
  145.         Request $request,
  146.         DashboardComponentRepository $repo,
  147.         EntityManagerInterface $em
  148.     ): JsonResponse {
  149.         $data json_decode($request->getContent(), true);
  150.         if (!isset($data['components'])) {
  151.             return new JsonResponse(['success' => false'error' => 'Invalid payload'], 400);
  152.         }
  153.         foreach ($data['components'] as $item) {
  154.             if (!isset($item['id'])) continue;
  155.             $component $repo->find($item['id']);
  156.             if ($component) {
  157.                 if (isset($item['width']))  $component->setWidth($item['width']);
  158.                 if (isset($item['height'])) $component->setHeight($item['height']);
  159.                 if (isset($item['x']))      $component->setPositionX($item['x']);
  160.                 if (isset($item['y']))      $component->setPositionY($item['y']);
  161.                 $em->persist($component);
  162.             }
  163.         }
  164.         $em->flush();
  165.         return new JsonResponse(['success' => true]);
  166.     }
  167.     /**
  168.      * @Route("/dashboard_component/update", name="dashboard_component_update", methods={"POST"})
  169.      */
  170.     public function update(Request $requestEntityManagerInterface $emDashboardComponentRepository $repo): JsonResponse
  171.     {
  172.         $id $request->request->get('id');
  173.         $component $repo->find($id);
  174.         if (!$component) {
  175.             return new JsonResponse(['success' => false], 404);
  176.         }
  177.         $component->setTitle($request->request->get('title'));
  178.         $component->setTextColor($request->request->get('text_color'));
  179.         $component->setBackgroundColor($request->request->get('background_color'));
  180.         $component->setWidth((int) $request->request->get('width'));
  181.         $component->setHeight((int) $request->request->get('height'));
  182.         $em->flush();
  183.         return new JsonResponse(['success' => true]);
  184.     }
  185.     /**
  186.      * @Route("/dashboard_component/delete/{id}", name="dashboard_component_delete", methods={"POST"})
  187.      */
  188.     public function deleteComponent(DashboardComponent $componentEntityManagerInterface $em): JsonResponse
  189.     {
  190.         try {
  191.             $em->remove($component);
  192.             $em->flush();
  193.             return new JsonResponse(['success' => true]);
  194.         } catch (\Exception $e) {
  195.             return new JsonResponse(['success' => false'message' => $e->getMessage()], 500);
  196.         }
  197.     }
  198.     /**
  199.      * @Route("/dashboard_component/table/{id}/embed", name="dashboard_component_embed_table", methods={"GET"})
  200.      */
  201.     public function embedTable(
  202.         int $id,
  203.         DashboardComponentRepository $repo,
  204.         TGrafikRepository $grafikRepo
  205.     ): Response {
  206.         $component $repo->find($id);
  207.         $grafikId $component->getConfig()['external_chart_id'] ?? null;
  208.         $grafik $grafikRepo->find($grafikId);
  209.         $columns $this->getGrafikColumns($grafik);
  210.         return $this->render('backend/t_grafik/_embed_table.html.twig', [
  211.             'grafik' => $grafik,
  212.             'columns' => $columns,
  213.         ]);
  214.     }
  215.     private function getGrafikColumns(TGrafik $grafik): array
  216.     {
  217.         $sumbu_x = ($grafik->getAxisx() === 'kategori_plant') ? 'plant' $grafik->getAxisx();
  218.         $sumbu_y $grafik->getAxisYIds();
  219.         if ($grafik->getAxisx() == 'all') {
  220.             return $sumbu_y// jika axis_x adalah 'all', hanya kembalikan sumbu_y
  221.         }
  222.         return array_merge([$sumbu_x], $sumbu_y);
  223.     }
  224.     /**
  225.      * @Route("/dashboard_component/data-source/preview-fields/{id}", name="dashboard_component_preview_fields", methods={"GET"})
  226.      */
  227.     public function previewFields(
  228.         int $id,
  229.         TGrafikRepository $repo,
  230.         EntityManagerInterface $em
  231.     ): JsonResponse {
  232.         $grafik $repo->find($id);
  233.         if (!$grafik || !$grafik->getTabel()) {
  234.             return new JsonResponse(['success' => false'message' => 'Data source tidak valid']);
  235.         }
  236.         $table $grafik->getTabel();
  237.         $sql 'SELECT * FROM "' str_replace('"'''$table) . '" LIMIT 1';
  238.         try {
  239.             $conn $em->getConnection();
  240.             $stmt $conn->prepare($sql);
  241.             $result $stmt->executeQuery();
  242.             $row $result->fetchAssociative();
  243.             // Cek field valid
  244.             $fields $row array_keys($row) : [];
  245.             return new JsonResponse([
  246.                 'success' => true,
  247.                 'fields' => $fields // hanya kunci yang benar-benar ada
  248.             ]);
  249.         } catch (\Throwable $e) {
  250.             return new JsonResponse([
  251.                 'success' => false,
  252.                 'error' => $e->getMessage()
  253.             ]);
  254.         }
  255.     }
  256.     // /**
  257.     //  * @Route("/dashboard_component/embed_table_by_data_source/{id}", name="dashboard_component_embed_table_by_data_source", methods={"GET"})
  258.     //  */
  259.     // public function embedTableByDataSource(
  260.     //     int $id,
  261.     //     TGrafikRepository $grafikRepo,
  262.     //     EntityManagerInterface $em
  263.     // ): Response {
  264.     //     $grafik = $grafikRepo->find($id);
  265.     //     if (!$grafik) {
  266.     //         throw $this->createNotFoundException('Data source tidak ditemukan');
  267.     //     }
  268.     //     $columns = $this->getGrafikColumns($grafik);
  269.     //     // Ambil data dari SQL
  270.     //     $conn = $em->getConnection();
  271.     //     $table = $grafik->getTabel();
  272.     //     $axisX = $grafik->getAxisx() === 'kategori_plant' ? 'plant' : $grafik->getAxisx();
  273.     //     $axisY = $grafik->getAxisYIds(); // array
  274.     //     $operation = $grafik->getOperation() ?: 'COUNT';
  275.     //     $selectParts = [];
  276.     //     foreach ($axisY as $col) {
  277.     //         $selectParts[] = "$operation(\"$col\") AS \"$col\"";
  278.     //     }
  279.     //     if ($grafik->getAxisx() == 'all') {
  280.     //         $selectClause = implode(", ", $selectParts);
  281.     //         $sql = "SELECT $selectClause FROM \"$table\" LIMIT 10";
  282.     //     } else {
  283.     //         $selectClause = "\"$axisX\", " . implode(", ", $selectParts);
  284.     //         $sql = "SELECT $selectClause FROM \"$table\" GROUP BY \"$axisX\" ORDER BY \"$axisX\" LIMIT 10";
  285.     //     }
  286.     //     try {
  287.     //         $stmt = $conn->prepare($sql);
  288.     //         $rows = $stmt->executeQuery()->fetchAllAssociative();
  289.     //     } catch (\Throwable $e) {
  290.     //         $rows = [];
  291.     //     }
  292.     //     return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
  293.     //         'grafik' => $grafik,
  294.     //         'columns' => $columns,
  295.     //         'data' => $rows
  296.     //     ]);
  297.     // }
  298.     /**
  299.      * @Route("/dashboard_component/embed_table_by_data_source/{id}", name="dashboard_component_embed_table_by_data_source", methods={"GET"})
  300.      */
  301.     public function embedTableByDataSource(
  302.         int $id,
  303.         Request $request,
  304.         TGrafikRepository $grafikRepo,
  305.         TExploreDataDetailRepository $exploreRepo,
  306.         EntityManagerInterface $em
  307.     ): Response {
  308.         $type $request->query->get('type');
  309.         // ----------------------------------------------------------------------
  310.         // Sumber: Analytics / t_explore_data_detail  -> SUM(nilai) per periode
  311.         // ----------------------------------------------------------------------
  312.         if ($type === 't_explore_data_detail') {
  313.             // boleh lewat param ?explorasi=... atau fallback ke {id}
  314.             $exploreId $request->query->get('explorasi'$id);
  315.             $explore   $exploreRepo->find($exploreId);
  316.             if (!$explore) {
  317.                 return new Response("Data source Analytics tidak ditemukan"404);
  318.             }
  319.             // tentukan sumber: view atau tabel
  320.             $tableOrView $explore->isIsCreateView() ? $explore->getView() : $explore->getTabel();
  321.             if (!$tableOrView) {
  322.                 return new Response("View/Tabel untuk Analytics belum diset"400);
  323.             }
  324.             $ident preg_replace('/[^A-Za-z0-9_\.]/'''$tableOrView);
  325.             $parts explode('.'$ident);
  326.             $qname implode('.'array_map(fn($p) => '"' str_replace('"''""'$p) . '"'$parts));
  327.             $conn $em->getConnection();
  328.             $where = ["COALESCE(t.periode,'') <> ''"];
  329.             $filtersFromExplore $explore->getFilter();
  330.             if (!$explore->isIsCreateView() && is_array($filtersFromExplore) && !empty($filtersFromExplore)) {
  331.                 $where[] = '(' implode(' AND '$filtersFromExplore) . ')';
  332.             }
  333.             $uiFilters $request->query->all('filters') ?? [];
  334.             $params = [];
  335.             if (!empty($uiFilters['periode'])) {
  336.                 $where[] = "t.periode ILIKE :periode";
  337.                 $params['periode'] = '%' $uiFilters['periode'] . '%';
  338.             }
  339.             if (!empty($uiFilters['nilai'])) {
  340.                 $where[] = "t.nilai::text ILIKE :nilai";
  341.                 $params['nilai'] = '%' $uiFilters['nilai'] . '%';
  342.             }
  343.             $whereSql $where ? ('WHERE ' implode(' AND '$where)) : '';
  344.             $orderSql "
  345.             ORDER BY 
  346.                 TO_DATE(LEFT(t.periode, 3) || ' ' || SUBSTRING(t.periode, 4, 4), 'Mon YYYY') ASC,
  347.                 CASE SPLIT_PART(t.periode, '.', 2)
  348.                     WHEN 'I'   THEN 1
  349.                     WHEN 'II'  THEN 2
  350.                     WHEN 'III' THEN 3
  351.                     WHEN 'IV'  THEN 4
  352.                     ELSE 0
  353.                 END ASC
  354.         ";
  355.             // preview: 10 baris
  356.             $limit  10;
  357.             $offset 0;
  358.             $sql "
  359.             SELECT 
  360.                 t.periode,
  361.                 SUM(t.nilai) AS nilai
  362.             FROM {$qname} t
  363.             {$whereSql}
  364.             GROUP BY t.periode
  365.             {$orderSql}
  366.             LIMIT {$limit} OFFSET {$offset}
  367.         ";
  368.             try {
  369.                 $rows $conn->executeQuery($sql$params)->fetchAllAssociative();
  370.             } catch (\Throwable $e) {
  371.                 $rows = [];
  372.             }
  373.             $columns = ['periode''nilai'];
  374.             return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
  375.                 'columns' => $columns,
  376.                 'data'    => $rows,
  377.             ]);
  378.         }
  379.         // ---------------------------------------
  380.         // Sumber: t_grafik (preview data source) 
  381.         // ---------------------------------------
  382.         $grafik $grafikRepo->find($id);
  383.         if (!$grafik) {
  384.             return new Response("Data grafik tidak ditemukan"404);
  385.         }
  386.         $conn      $em->getConnection();
  387.         $table     $grafik->getTabel();
  388.         $axisXConf $grafik->getAxisx();
  389.         $axisX     $axisXConf === 'kategori_plant' 'plant' $axisXConf;
  390.         $axisY     $grafik->getAxisYIds() ?: [];
  391.         $operation strtoupper(trim($grafik->getOperation() ?: 'COUNT'));
  392.         $quoteIdent = function (?string $ident): string {
  393.             $ident preg_replace('/[^A-Za-z0-9_\.]/'''$ident ?? '');
  394.             $parts array_filter(explode('.'$ident), fn($p) => $p !== '');
  395.             return implode('.'array_map(fn($p) => '"' str_replace('"''""'$p) . '"'$parts));
  396.         };
  397.         $qTable $quoteIdent($table);
  398.         $buildAgg = function (string $opstring $col) use ($quoteIdent) {
  399.             $qCol $quoteIdent($col);
  400.             switch ($op) {
  401.                 case 'COUNT_DISTINCT':
  402.                     return sprintf('COUNT(DISTINCT %s) AS "%s"'$qColstr_replace('"''""'$col));
  403.                 case 'SUM':
  404.                 case 'AVG':
  405.                 case 'MIN':
  406.                 case 'MAX':
  407.                 case 'COUNT':
  408.                 default:
  409.                     return sprintf('%s(%s) AS "%s"'$op$qColstr_replace('"''""'$col));
  410.             }
  411.         };
  412.         // =========================
  413.         // axisx = 'all'
  414.         // =========================
  415.         if ($axisXConf === 'all') {
  416.             // Seleksi hanya agregat per kolom Y -> 1 baris
  417.             $selectParts = [];
  418.             if (empty($axisY)) {
  419.                 $selectParts[] = 'COUNT(*) AS total';
  420.                 $columns = ['total'];
  421.             } else {
  422.                 foreach ($axisY as $col) {
  423.                     $selectParts[] = $buildAgg($operation$col);
  424.                 }
  425.                 $columns $axisY// header mengikuti nama kolom axisY
  426.             }
  427.             $sql sprintf('SELECT %s FROM %s'implode(', '$selectParts), $qTable);
  428.             try {
  429.                 $rows $conn->executeQuery($sql)->fetchAllAssociative();
  430.             } catch (\Throwable $e) {
  431.                 $rows = [];
  432.             }
  433.             return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
  434.                 'columns' => $columns,
  435.                 'data'    => $rows,
  436.             ]);
  437.         }
  438.         // =========================
  439.         // axisx ≠ 'all'
  440.         // =========================
  441.         $selectParts = [];
  442.         if (empty($axisY)) {
  443.             $selectParts[] = 'COUNT(*) AS total';
  444.             $columns = [$axisX'total'];
  445.         } else {
  446.             foreach ($axisY as $col) {
  447.                 $selectParts[] = $buildAgg($operation$col);
  448.             }
  449.             $columns array_merge([$axisX], $axisY);
  450.         }
  451.         $qAxisX $quoteIdent($axisX);
  452.         $sql sprintf(
  453.             'SELECT %s, %s FROM %s GROUP BY %s ORDER BY %s LIMIT 10',
  454.             $qAxisX,
  455.             implode(', '$selectParts),
  456.             $qTable,
  457.             $qAxisX,
  458.             $qAxisX
  459.         );
  460.         try {
  461.             $rows $conn->executeQuery($sql)->fetchAllAssociative();
  462.         } catch (\Throwable $e) {
  463.             $rows = [];
  464.         }
  465.         return $this->render('backend/t_grafik/_embed_table_html_prev.html.twig', [
  466.             'columns' => $columns,
  467.             'data'    => $rows,
  468.         ]);
  469.     }
  470.     /**
  471.      * @Route("/dashboard_component/{id}/get_html_json", name="dashboard_component_get_html_json", methods={"GET"})
  472.      */
  473.     public function getHtmlJson(
  474.         int $id,
  475.         DashboardComponentRepository $repo,
  476.         TGrafikRepository $tGrafikRepo,
  477.         EntityManagerInterface $em
  478.     ): JsonResponse {
  479.         $component $repo->find($id);
  480.         $grafikId $component->getDataSource();
  481.         $tGrafik $tGrafikRepo->find($grafikId);
  482.         if (!$tGrafik) {
  483.             return new JsonResponse([]);
  484.         }
  485.         $conn $em->getConnection();
  486.         $table $tGrafik->getTabel();
  487.         $axisX $tGrafik->getAxisx() === 'kategori_plant' 'plant' $tGrafik->getAxisx();
  488.         $axisY $tGrafik->getAxisYIds(); // array
  489.         $operation $tGrafik->getOperation() ?: 'COUNT';
  490.         $columns array_merge([$axisX], $axisY);
  491.         $selectParts = [];
  492.         foreach ($axisY as $col) {
  493.             if ($tGrafik->getOperation() == 'COUNT_DISTINCT') {
  494.                 $selectParts[] = "COUNT(DISTINCT(\"$col\")) AS \"$col\"";
  495.             } else {
  496.                 $selectParts[] = "$operation(\"$col\") AS \"$col\"";
  497.             }
  498.         }
  499.         if ($tGrafik->getAxisx() == 'all') {
  500.             $selectClause implode(", "$selectParts);
  501.             $sql "SELECT $selectClause FROM \"$table\" LIMIT 1";
  502.         } else {
  503.             $selectClause "\"$axisX\", " implode(", "$selectParts);
  504.             $sql "SELECT $selectClause FROM \"$table\" GROUP BY \"$axisX\" ORDER BY \"$axisX\" LIMIT 1";
  505.         }
  506.         try {
  507.             $stmt $conn->prepare($sql);
  508.             $data $stmt->executeQuery()->fetchAllAssociative();
  509.         } catch (\Throwable $e) {
  510.             $data = [];
  511.         }
  512.         return new JsonResponse($data);
  513.     }
  514.     /**
  515.      * @Route("/dashboard_component/explore/chart-data/{id}", name="dashboard_component_chart_data_explore", methods={"GET"})
  516.      */
  517.     public function getExploreChartData(
  518.         int $id,
  519.         DashboardComponentRepository $repo,
  520.         TExploreDataDetailRepository $exploreRepo,
  521.         Request $request
  522.     ): JsonResponse {
  523.         $comp $repo->find($id);
  524.         $config $comp->getConfig();
  525.         $exploreId $config['external_chart_id'] ?? null;
  526.         $xAxis $config['x_axis'] ?? 'periode';
  527.         $yAxis $config['y_axis'] ?? 'nilai';
  528.         $chartType $config['chart_type'] ?? 'column';
  529.         $explore $exploreRepo->find($exploreId);
  530.         if (!$explore) {
  531.             return $this->json(['error' => 'Sumber data eksplorasi tidak ditemukan'], 404);
  532.         }
  533.         // Ambil nama tabel atau view
  534.         $table $explore->isIsCreateView() ? $explore->getView() : $explore->getTabel();
  535.         // Kirim semua data
  536.         $filters = []; // kamu bisa isi dari GET kalau mau, tapi kosongkan jika chart tidak pakai filter
  537.         $data $this->getDataTableDetailExplorasiPeriode(nullnull$filters$table$exploreId);
  538.         if (empty($data)) {
  539.             return $this->json(['error' => 'Data eksplorasi kosong'], 400);
  540.         }
  541.         if (!isset($data[0][$xAxis]) || !isset($data[0][$yAxis])) {
  542.             return $this->json(['error' => "Kolom '$xAxis' atau '$yAxis' tidak ditemukan"], 400);
  543.         }
  544.         $categories array_column($data$xAxis);
  545.         $series array_map(fn($row) => (float) $row[$yAxis], $data);
  546.         return $this->json([
  547.             'chart_type' => $chartType,
  548.             'x_axis' => $xAxis,
  549.             'y_axis' => $yAxis,
  550.             'columns' => $categories,
  551.             'datas' => [[
  552.                 'name' => $yAxis,
  553.                 'data' => $series
  554.             ]]
  555.         ]);
  556.     }
  557. }