diff --git a/composer.json b/composer.json index 51a8933..b9d9a63 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,11 @@ "require-dev": { "prestashop/php-dev-tools": "^3.4" }, + "autoload": { + "psr-4": { + "PrestaShop\\Module\\DashActivity\\": "src/" + } + }, "config": { "preferred-install": "dist", "classmap-authoritative": true, diff --git a/config.xml b/config.xml index 1e5e281..569bd3f 100644 --- a/config.xml +++ b/config.xml @@ -2,11 +2,11 @@ dashactivity - - + + - - 0 + + 1 1 diff --git a/config/index.php b/config/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/config/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/config/routes.yml b/config/routes.yml new file mode 100644 index 0000000..5f78a45 --- /dev/null +++ b/config/routes.yml @@ -0,0 +1,7 @@ +dashactivity_configuration: + path: /dashactivity/configuration + methods: [GET, POST] + defaults: + _controller: PrestaShop\Module\DashActivity\Controller\ConfigurationController::indexAction + _legacy_controller: AdminDashactivityConfiguration + _legacy_link: AdminDashactivityConfiguration diff --git a/config/services.yml b/config/services.yml new file mode 100644 index 0000000..3a7134e --- /dev/null +++ b/config/services.yml @@ -0,0 +1,10 @@ +services: + PrestaShop\Module\DashActivity\Controller\ConfigurationController: + public: true + autowire: true + autoconfigure: true + + PrestaShop\Module\DashActivity\Type\ConfigurationType: + autowire: true + autoconfigure: true + tags: ['form.type'] diff --git a/dashactivity.php b/dashactivity.php index 9e725fc..32a4b2f 100755 --- a/dashactivity.php +++ b/dashactivity.php @@ -23,6 +23,9 @@ * @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) */ +use PrestaShop\PrestaShop\Adapter\SymfonyContainer; +use Twig\Environment; + if (!defined('_PS_VERSION_')) { exit; } @@ -35,7 +38,7 @@ public function __construct() { $this->name = 'dashactivity'; $this->tab = 'administration'; - $this->version = '2.1.2'; + $this->version = '3.0.0'; $this->author = 'PrestaShop'; parent::__construct(); @@ -47,17 +50,61 @@ public function __construct() public function install() { Configuration::updateValue('DASHACTIVITY_CART_ACTIVE', 30); - Configuration::updateValue('DASHACTIVITY_CART_ABANDONED_MIN', 24); - Configuration::updateValue('DASHACTIVITY_CART_ABANDONED_MAX', 48); + // In minutes (see hookDashboardData()'s strtotime('- X MIN')), not hours: 1440/2880 is 24h/48h. + Configuration::updateValue('DASHACTIVITY_CART_ABANDONED_MIN', 1440); + Configuration::updateValue('DASHACTIVITY_CART_ABANDONED_MAX', 2880); Configuration::updateValue('DASHACTIVITY_VISITOR_ONLINE', 30); return parent::install() + && $this->createConfigurationTab() && $this->registerHook('dashboardZoneOne') && $this->registerHook('dashboardData') && $this->registerHook('actionAdminControllerSetMedia') + // Modern counterpart of dashboardZoneOne, registered alongside it (#41971). + && $this->registerHook('displayAdminDashboardZoneOne') ; } + public function uninstall() + { + $idTab = (int) Tab::getIdFromClassName('AdminDashactivityConfiguration'); + if ($idTab) { + (new Tab($idTab))->delete(); + } + + return parent::uninstall(); + } + + /** + * Hidden tab (id_parent -1): only used to back the settings route's ACL. Shared between + * install() and the 3.0.0 upgrade script so shops upgrading from an earlier version get it too. + */ + public function createConfigurationTab(): bool + { + if (Tab::getIdFromClassName('AdminDashactivityConfiguration')) { + return true; + } + + $tab = new Tab(); + $tab->active = true; + $tab->class_name = 'AdminDashactivityConfiguration'; + $tab->name = []; + foreach (Language::getLanguages(true) as $lang) { + $tab->name[$lang['id_lang']] = 'Dashactivity configuration'; + } + $tab->id_parent = -1; + $tab->module = $this->name; + + return $tab->add(); + } + + public function getContent() + { + Tools::redirectAdmin( + SymfonyContainer::getInstance()->get('router')->generate('dashactivity_configuration') + ); + } + public function hookActionAdminControllerSetMedia() { if (get_class($this->context->controller) == 'AdminDashboardController') { @@ -80,6 +127,102 @@ public function hookDashboardZoneOne($params) return $this->display(__FILE__, 'dashboard_zone_one.tpl'); } + /** + * Modern counterpart of hookDashboardZoneOne(). Reuses hookDashboardData() as-is. + */ + public function hookDisplayAdminDashboardZoneOne(array $params) + { + $data = $this->hookDashboardData($params); + + return $this->render('zone_one.html.twig', [ + 'kpiTitle' => $this->trans('Activity overview', [], 'Modules.Dashactivity.Admin'), + 'kpiItems' => $this->getKpiList($data['data_value']), + 'configUrl' => $this->getConfigUrl(), + 'chartTitle' => $this->trans('Traffic Sources', [], 'Modules.Dashactivity.Admin'), + 'chartId' => 'dashactivity-traffic-sources', + 'chartConfig' => $this->getTrafficSourcesChartConfig($data['data_chart']['dash_trends_chart1'] ?? []), + ]); + } + + private function render(string $template, array $params = []): string + { + $twig = $this->get('twig'); + if (!$twig instanceof Environment) { + return ''; + } + + return $twig->render('@Modules/dashactivity/views/templates/admin/' . $template, $params); + } + + /** + * Null (no "Configure" link shown) when the current employee can't configure this module. + */ + private function getConfigUrl(): ?string + { + if (!Tab::checkTabRights((int) Tab::getIdFromClassName('AdminDashactivityConfiguration'))) { + return null; + } + + return SymfonyContainer::getInstance()->get('router')->generate('dashactivity_configuration'); + } + + /** + * Plain label/value pairs — no JS contract needed, this is server-rendered HTML. + */ + private function getKpiList(array $dataValue): array + { + $labels = [ + 'pending_orders' => $this->trans('Orders', [], 'Admin.Global'), + 'return_exchanges' => $this->trans('Return/Exchanges', [], 'Modules.Dashactivity.Admin'), + 'abandoned_cart' => $this->trans('Abandoned Carts', [], 'Admin.Global'), + 'products_out_of_stock' => $this->trans('Out of Stock Products', [], 'Modules.Dashactivity.Admin'), + 'new_messages' => $this->trans('New Messages', [], 'Modules.Dashactivity.Admin'), + 'product_reviews' => $this->trans('Product Reviews', [], 'Modules.Dashactivity.Admin'), + 'new_customers' => $this->trans('New Customers', [], 'Modules.Dashactivity.Admin'), + 'online_visitor' => $this->trans('Online Visitors', [], 'Modules.Dashactivity.Admin'), + 'active_shopping_cart' => $this->trans('Active Shopping Carts', [], 'Modules.Dashactivity.Admin'), + 'new_registrations' => $this->trans('New Subscriptions', [], 'Modules.Dashactivity.Admin'), + 'total_suscribers' => $this->trans('Total Subscribers', [], 'Modules.Dashactivity.Admin'), + 'visits' => $this->trans('Visits', [], 'Modules.Dashactivity.Admin'), + 'unique_visitors' => $this->trans('Unique Visitors', [], 'Modules.Dashactivity.Admin'), + ]; + + $kpis = []; + foreach ($labels as $key => $label) { + if (array_key_exists($key, $dataValue)) { + $kpis[] = ['label' => $label, 'value' => $dataValue[$key]]; + } + } + + return $kpis; + } + + /** + * Plain Chart.js config, no dataset color (auto-applied by the core psColors plugin). + */ + private function getTrafficSourcesChartConfig(array $nvd3Chart): array + { + $labels = []; + $values = []; + foreach ($nvd3Chart['data'] ?? [] as $point) { + $labels[] = $point['key']; + $values[] = $point['y']; + } + + return [ + 'type' => 'doughnut', + 'data' => [ + 'labels' => $labels, + 'datasets' => [ + ['data' => $values], + ], + ], + 'options' => [ + 'plugins' => ['legend' => ['position' => 'bottom']], + ], + ]; + } + public function hookDashboardData($params) { if (Tools::strlen($params['date_from']) == 10) { @@ -398,17 +541,17 @@ public function renderConfigForm() ]; $fields_form['form']['input'][] = [ 'label' => $this->trans('Abandoned cart (min)', [], 'Modules.Dashactivity.Admin'), - 'hint' => $this->trans('How long (in hours) after the last action a cart is to be considered as abandoned (default: 24 hrs).', [], 'Modules.Dashactivity.Admin'), + 'hint' => $this->trans('How long (in minutes) after the last action a cart is to be considered as abandoned (default: 1440 min, 24h).', [], 'Modules.Dashactivity.Admin'), 'name' => 'DASHACTIVITY_CART_ABANDONED_MIN', 'type' => 'text', - 'suffix' => $this->trans('hrs', [], 'Modules.Dashactivity.Admin'), + 'suffix' => $this->trans('min', [], 'Modules.Dashactivity.Admin'), ]; $fields_form['form']['input'][] = [ 'label' => $this->trans('Abandoned cart (max)', [], 'Modules.Dashactivity.Admin'), - 'hint' => $this->trans('How long (in hours) after the last action a cart is no longer to be considered as abandoned (default: 24 hrs).', [], 'Modules.Dashactivity.Admin'), + 'hint' => $this->trans('How long (in minutes) after the last action a cart is no longer to be considered as abandoned (default: 2880 min, 48h).', [], 'Modules.Dashactivity.Admin'), 'name' => 'DASHACTIVITY_CART_ABANDONED_MAX', 'type' => 'text', - 'suffix' => $this->trans('hrs', [], 'Modules.Dashactivity.Admin'), + 'suffix' => $this->trans('min', [], 'Modules.Dashactivity.Admin'), ]; $helper = new HelperForm(); diff --git a/src/Controller/ConfigurationController.php b/src/Controller/ConfigurationController.php new file mode 100644 index 0000000..8b2e5b7 --- /dev/null +++ b/src/Controller/ConfigurationController.php @@ -0,0 +1,92 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\DashActivity\Controller; + +use Configuration; +use PrestaShop\Module\DashActivity\Type\ConfigurationType; +// FrameworkBundleAdminController is deprecated since 9.0 in favor of PrestaShopAdminController, +// which doesn't exist on PS 8.2 — kept for the module's declared 8.2+ compatibility. +use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController; +use PrestaShopBundle\Security\Annotation\AdminSecurity; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class ConfigurationController extends FrameworkBundleAdminController +{ + private const FIELDS = [ + 'DASHACTIVITY_CART_ACTIVE', + 'DASHACTIVITY_VISITOR_ONLINE', + 'DASHACTIVITY_CART_ABANDONED_MIN', + 'DASHACTIVITY_CART_ABANDONED_MAX', + ]; + + // Every install() before 3.0.0 set these two exact values, meant as hours (the free-text + // field's own former label) though always read back as minutes — never anything else, since + // no UI let a merchant change them before this settings page existed. + private const LEGACY_ABANDONED_CART_MIN_DEFAULT = 24; + private const LEGACY_ABANDONED_CART_MAX_DEFAULT = 48; + + /** + * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))") + */ + public function indexAction(Request $request): Response + { + $data = []; + foreach (self::FIELDS as $field) { + $data[$field] = (int) Configuration::get($field); + } + + // Correct the known pre-3.0.0 default to what it always meant (1440 min = 24h, 2880 min + // = 48h) for display only: nothing is written back unless the merchant saves the form. + if (self::LEGACY_ABANDONED_CART_MIN_DEFAULT === $data['DASHACTIVITY_CART_ABANDONED_MIN']) { + $data['DASHACTIVITY_CART_ABANDONED_MIN'] = 1440; + } + if (self::LEGACY_ABANDONED_CART_MAX_DEFAULT === $data['DASHACTIVITY_CART_ABANDONED_MAX']) { + $data['DASHACTIVITY_CART_ABANDONED_MAX'] = 2880; + } + + $form = $this->createForm(ConfigurationType::class, $data); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->denyAccessUnlessGranted('update', $request->attributes->get('_legacy_controller')); + + // No @DemoRestricted attribute here: its listener only recognizes the PHP 8 + // attribute form (PrestaShopBundle\Security\Attribute\DemoRestricted), which + // doesn't exist on PS 8.2 — this module's declared minimum version. + if ($this->isDemoModeEnabled()) { + $this->addFlash('error', $this->trans('This functionality has been disabled.', 'Admin.Notifications.Error')); + + return $this->redirectToRoute('dashactivity_configuration'); + } + + foreach ($form->getData() as $field => $value) { + Configuration::updateValue($field, (int) $value); + } + $this->addFlash('success', $this->trans('Successful update.', 'Admin.Notifications.Success')); + + return $this->redirectToRoute('dashactivity_configuration'); + } + + return $this->render('@Modules/dashactivity/views/templates/admin/configuration.html.twig', [ + 'configurationForm' => $form->createView(), + 'enableSidebar' => true, + 'help_link' => $this->generateSidebarLink('AdminDashactivityConfiguration'), + ]); + } +} diff --git a/src/Controller/index.php b/src/Controller/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/src/Controller/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/src/Type/ConfigurationType.php b/src/Type/ConfigurationType.php new file mode 100644 index 0000000..89319f3 --- /dev/null +++ b/src/Type/ConfigurationType.php @@ -0,0 +1,103 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\DashActivity\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Plain AbstractType (no need for TranslatorAwareType's locales), with TranslatorInterface + * constructor-injected directly so label/help strings go through an explicit trans() call — + * required for the translation extractor to pick them up, since it only extracts ChoiceType + * "choices", not "label"/"help" option strings. + */ +class ConfigurationType extends AbstractType +{ + private const DELAY_CHOICES = [15, 30, 45, 60, 90, 120]; + + // DASHACTIVITY_CART_ABANDONED_MIN/MAX are consumed as minutes (strtotime('- X MIN') in + // dashactivity.php), not hours as the field used to suggest — 30 min to 3 days covers the + // range a store would realistically want for a cart abandonment window. + private const ABANDONED_CART_CHOICES = [30, 60, 120, 240, 360, 720, 1440, 2880, 4320]; + + private const DOMAIN = 'Modules.Dashactivity.Admin'; + + /** @var TranslatorInterface */ + private $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $delayChoices = []; + foreach (self::DELAY_CHOICES as $minutes) { + $delayChoices[self::formatMinutesChoiceLabel($minutes)] = $minutes; + } + $abandonedCartChoices = []; + foreach (self::ABANDONED_CART_CHOICES as $minutes) { + $abandonedCartChoices[self::formatMinutesChoiceLabel($minutes)] = $minutes; + } + + $builder + ->add('DASHACTIVITY_CART_ACTIVE', ChoiceType::class, [ + 'label' => $this->translator->trans('Active cart', [], self::DOMAIN), + 'help' => $this->translator->trans('How long (in minutes) a cart is to be considered as active after the last recorded change (default: 30 min).', [], self::DOMAIN), + 'choices' => $delayChoices, + ]) + ->add('DASHACTIVITY_VISITOR_ONLINE', ChoiceType::class, [ + 'label' => $this->translator->trans('Online visitor', [], self::DOMAIN), + 'help' => $this->translator->trans('How long (in minutes) a visitor is to be considered as online after their last action (default: 30 min).', [], self::DOMAIN), + 'choices' => $delayChoices, + ]) + ->add('DASHACTIVITY_CART_ABANDONED_MIN', ChoiceType::class, [ + 'label' => $this->translator->trans('Abandoned cart (min)', [], self::DOMAIN), + 'help' => $this->translator->trans('How long (in minutes) after the last action a cart is to be considered as abandoned (default: 1440 min, 24h).', [], self::DOMAIN), + 'choices' => $abandonedCartChoices, + ]) + ->add('DASHACTIVITY_CART_ABANDONED_MAX', ChoiceType::class, [ + 'label' => $this->translator->trans('Abandoned cart (max)', [], self::DOMAIN), + 'help' => $this->translator->trans('How long (in minutes) after the last action a cart is no longer to be considered as abandoned (default: 2880 min, 48h).', [], self::DOMAIN), + 'choices' => $abandonedCartChoices, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'required' => true, + 'translation_domain' => 'Modules.Dashactivity.Admin', + ]); + } + + // e.g. 1440 -> "24h", 30 -> "30 min". + private static function formatMinutesChoiceLabel(int $minutes): string + { + if ($minutes >= 60 && 0 === $minutes % 60) { + return sprintf('%dh', intdiv($minutes, 60)); + } + + return sprintf('%d min', $minutes); + } +} diff --git a/src/Type/index.php b/src/Type/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/src/Type/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/src/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/tests/php/phpstan/phpstan.neon b/tests/php/phpstan/phpstan.neon index 018ab98..0cb7f98 100644 --- a/tests/php/phpstan/phpstan.neon +++ b/tests/php/phpstan/phpstan.neon @@ -1,2 +1,10 @@ # Base PHPStan configuration for the module - # This file is included by version-specific configs \ No newline at end of file + # This file is included by version-specific configs + +parameters: + # Reset to auto-detection: the shared CI action injects a phpVersion computed from the + # matrix's php-version input, which some older PrestaShop releases' pinned PHPStan can't + # parse once that input reaches a version newer than its own release date (PHPStan's + # phpVersion schema is capped at whatever PHP existed when that PHPStan version shipped). + # Included last, so this wins over the injected value (see PrestaShop/.github#phpstan). + phpVersion: null diff --git a/upgrade/index.php b/upgrade/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/upgrade/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/upgrade/upgrade-3.0.0.php b/upgrade/upgrade-3.0.0.php new file mode 100644 index 0000000..53c1010 --- /dev/null +++ b/upgrade/upgrade-3.0.0.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +if (!defined('_PS_VERSION_')) { + exit; +} + +function upgrade_module_3_0_0($object) +{ + return $object->createConfigurationTab() + && $object->registerHook('displayAdminDashboardZoneOne'); +} diff --git a/views/templates/admin/configuration.html.twig b/views/templates/admin/configuration.html.twig new file mode 100644 index 0000000..cc3006e --- /dev/null +++ b/views/templates/admin/configuration.html.twig @@ -0,0 +1,31 @@ +{#** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * This source file is subject to the Academic Free License 3.0 (AFL-3.0). + * It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + *#} +{% extends '@PrestaShop/Admin/layout.html.twig' %} + +{% block content %} + {{ form_start(configurationForm) }} +
+
+
+

+ settings + {{ 'Activity overview settings'|trans({}, 'Modules.Dashactivity.Admin') }} +

+
+ {{ form_widget(configurationForm) }} +
+ +
+
+
+ {{ form_end(configurationForm) }} +{% endblock %} diff --git a/views/templates/admin/index.php b/views/templates/admin/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/views/templates/admin/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/views/templates/admin/zone_one.html.twig b/views/templates/admin/zone_one.html.twig new file mode 100644 index 0000000..c8860fd --- /dev/null +++ b/views/templates/admin/zone_one.html.twig @@ -0,0 +1,26 @@ +{# + # For the full copyright and license information, please view the + # docs/licenses/LICENSE.txt file that was distributed with this source code. + #} +
+ {% component 'Card' with {title: kpiTitle, icon: 'insights', configUrl: configUrl} %} + {% block content %} + + {% endblock %} + {% endcomponent %} +
+ +{{ component('ChartCard', { + chartId: chartId, + config: chartConfig, + height: 280, + title: chartTitle, + icon: 'donut_small', +}) }}