From b8dbacbe50b1ac7b9e94d74cbbc8c66a7241561c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Thu, 17 Sep 2026 15:54:39 +0200 Subject: [PATCH 1/7] Feed the Symfony dashboard's Zone One hook with KPIs and a chart, add a settings page --- composer.json | 5 + config.xml | 6 +- config/index.php | 34 +++++ config/routes.yml | 7 + config/services.yml | 10 ++ dashactivity.php | 144 +++++++++++++++++- src/Controller/ConfigurationController.php | 68 +++++++++ src/Controller/index.php | 34 +++++ src/Type/ConfigurationType.php | 84 ++++++++++ src/Type/index.php | 34 +++++ src/index.php | 34 +++++ upgrade/index.php | 34 +++++ upgrade/upgrade-2.2.0.php | 34 +++++ views/templates/admin/configuration.html.twig | 31 ++++ views/templates/admin/index.php | 34 +++++ views/templates/admin/zone_one.html.twig | 29 ++++ 16 files changed, 618 insertions(+), 4 deletions(-) create mode 100644 config/index.php create mode 100644 config/routes.yml create mode 100644 config/services.yml create mode 100644 src/Controller/ConfigurationController.php create mode 100644 src/Controller/index.php create mode 100644 src/Type/ConfigurationType.php create mode 100644 src/Type/index.php create mode 100644 src/index.php create mode 100644 upgrade/index.php create mode 100644 upgrade/upgrade-2.2.0.php create mode 100644 views/templates/admin/configuration.html.twig create mode 100644 views/templates/admin/index.php create mode 100644 views/templates/admin/zone_one.html.twig 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..a798a48 100644 --- a/config.xml +++ b/config.xml @@ -2,10 +2,10 @@ dashactivity - - + + - + 0 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..6be57e7 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 = '2.2.0'; $this->author = 'PrestaShop'; parent::__construct(); @@ -52,12 +55,55 @@ public function install() 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 2.2.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 +126,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) { diff --git a/src/Controller/ConfigurationController.php b/src/Controller/ConfigurationController.php new file mode 100644 index 0000000..313f8c8 --- /dev/null +++ b/src/Controller/ConfigurationController.php @@ -0,0 +1,68 @@ + + * @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', + ]; + + /** + * @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); + } + + $form = $this->createForm(ConfigurationType::class, $data); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->denyAccessUnlessGranted('update', $request->attributes->get('_legacy_controller')); + + 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..184ee95 --- /dev/null +++ b/src/Type/ConfigurationType.php @@ -0,0 +1,84 @@ + + * @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\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\GreaterThan; +use Symfony\Component\Validator\Constraints\NotBlank; +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]; + + 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 = array_combine(self::DELAY_CHOICES, self::DELAY_CHOICES); + + $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', IntegerType::class, [ + 'label' => $this->translator->trans('Abandoned cart (min)', [], self::DOMAIN), + 'help' => $this->translator->trans('How long (in hours) after the last action a cart is to be considered as abandoned (default: 24 hrs).', [], self::DOMAIN), + 'constraints' => [new NotBlank(), new GreaterThan(0)], + ]) + ->add('DASHACTIVITY_CART_ABANDONED_MAX', IntegerType::class, [ + 'label' => $this->translator->trans('Abandoned cart (max)', [], self::DOMAIN), + 'help' => $this->translator->trans('How long (in hours) after the last action a cart is no longer to be considered as abandoned (default: 24 hrs).', [], self::DOMAIN), + 'constraints' => [new NotBlank(), new GreaterThan(0)], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'required' => true, + 'translation_domain' => 'Modules.Dashactivity.Admin', + ]); + } +} 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/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-2.2.0.php b/upgrade/upgrade-2.2.0.php new file mode 100644 index 0000000..c09177b --- /dev/null +++ b/upgrade/upgrade-2.2.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_2_2_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..6edafa7 --- /dev/null +++ b/views/templates/admin/zone_one.html.twig @@ -0,0 +1,29 @@ +{# + # For the full copyright and license information, please view the + # docs/licenses/LICENSE.txt file that was distributed with this source code. + #} +{% import '@PrestaShop/Admin/Dashboard/_partials/card_header.html.twig' as dashboard %} +{% import '@PrestaShop/Admin/Dashboard/_partials/chart.html.twig' as chart %} + +
+ {{ dashboard.renderCardHeader(kpiTitle, 'insights', configUrl) }} +
+
    + {% for item in kpiItems %} +
  • + {{ item.label }} + {{ item.value }} +
  • + {% endfor %} +
+
+
+ +
+
+

donut_small {{ chartTitle }}

+
+
+ {{ chart.renderChart(chartId, chartConfig, {height: 280, label: chartTitle}) }} +
+
From 6343bc4701962b7101b0ad01fbb2ceedb4fdb470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Thu, 17 Sep 2026 16:08:48 +0200 Subject: [PATCH 2/7] Mark the module as configurable in config.xml, now that it has a settings page --- config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.xml b/config.xml index a798a48..4d8699e 100644 --- a/config.xml +++ b/config.xml @@ -6,7 +6,7 @@ - 0 + 1 1
From e3c46a493f3edf93fc1a58018fd1eab81fa05836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Thu, 17 Sep 2026 16:35:02 +0200 Subject: [PATCH 3/7] Reset phpVersion in PHPStan config to avoid crashing on CI's PHP 8.5 injection --- tests/php/phpstan/phpstan.neon | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From 2cf5306dbe68d4844e1546c474a3e9310fe484fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Fri, 18 Sep 2026 14:28:34 +0200 Subject: [PATCH 4/7] Use the Card/ChartCard Twig components instead of the dashboard partials --- views/templates/admin/zone_one.html.twig | 43 +++++++++++------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/views/templates/admin/zone_one.html.twig b/views/templates/admin/zone_one.html.twig index 6edafa7..c8860fd 100644 --- a/views/templates/admin/zone_one.html.twig +++ b/views/templates/admin/zone_one.html.twig @@ -2,28 +2,25 @@ # For the full copyright and license information, please view the # docs/licenses/LICENSE.txt file that was distributed with this source code. #} -{% import '@PrestaShop/Admin/Dashboard/_partials/card_header.html.twig' as dashboard %} -{% import '@PrestaShop/Admin/Dashboard/_partials/chart.html.twig' as chart %} - -
- {{ dashboard.renderCardHeader(kpiTitle, 'insights', configUrl) }} -
-
    - {% for item in kpiItems %} -
  • - {{ item.label }} - {{ item.value }} -
  • - {% endfor %} -
-
+
+ {% component 'Card' with {title: kpiTitle, icon: 'insights', configUrl: configUrl} %} + {% block content %} +
    + {% for item in kpiItems %} +
  • + {{ item.label }} + {{ item.value }} +
  • + {% endfor %} +
+ {% endblock %} + {% endcomponent %}
-
-
-

donut_small {{ chartTitle }}

-
-
- {{ chart.renderChart(chartId, chartConfig, {height: 280, label: chartTitle}) }} -
-
+{{ component('ChartCard', { + chartId: chartId, + config: chartConfig, + height: 280, + title: chartTitle, + icon: 'donut_small', +}) }} From b3c65c6d35854dc82cf39ddb8861f0d8b4cb3c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Thu, 24 Sep 2026 14:56:21 +0200 Subject: [PATCH 5/7] Bump to 3.0.0, replace @DemoRestricted with a manual demo-mode check --- config.xml | 2 +- dashactivity.php | 2 +- src/Controller/ConfigurationController.php | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/config.xml b/config.xml index 4d8699e..569bd3f 100644 --- a/config.xml +++ b/config.xml @@ -2,7 +2,7 @@ dashactivity - + diff --git a/dashactivity.php b/dashactivity.php index 6be57e7..3d973d5 100755 --- a/dashactivity.php +++ b/dashactivity.php @@ -38,7 +38,7 @@ public function __construct() { $this->name = 'dashactivity'; $this->tab = 'administration'; - $this->version = '2.2.0'; + $this->version = '3.0.0'; $this->author = 'PrestaShop'; parent::__construct(); diff --git a/src/Controller/ConfigurationController.php b/src/Controller/ConfigurationController.php index 313f8c8..ac2c8cb 100644 --- a/src/Controller/ConfigurationController.php +++ b/src/Controller/ConfigurationController.php @@ -51,6 +51,15 @@ public function indexAction(Request $request): Response 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); } From 7cd5fb93b6db3f8875431180c828f9b836678633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Fri, 25 Sep 2026 13:46:42 +0200 Subject: [PATCH 6/7] Fix the abandoned cart delay unit (minutes, not hours), rename the upgrade script to 3.0.0 --- dashactivity.php | 15 ++++++------- src/Type/ConfigurationType.php | 21 +++++++++++-------- .../{upgrade-2.2.0.php => upgrade-3.0.0.php} | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) rename upgrade/{upgrade-2.2.0.php => upgrade-3.0.0.php} (96%) diff --git a/dashactivity.php b/dashactivity.php index 3d973d5..32a4b2f 100755 --- a/dashactivity.php +++ b/dashactivity.php @@ -50,8 +50,9 @@ 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() @@ -76,7 +77,7 @@ public function uninstall() /** * Hidden tab (id_parent -1): only used to back the settings route's ACL. Shared between - * install() and the 2.2.0 upgrade script so shops upgrading from an earlier version get it too. + * install() and the 3.0.0 upgrade script so shops upgrading from an earlier version get it too. */ public function createConfigurationTab(): bool { @@ -540,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/Type/ConfigurationType.php b/src/Type/ConfigurationType.php index 184ee95..c4654a4 100644 --- a/src/Type/ConfigurationType.php +++ b/src/Type/ConfigurationType.php @@ -19,11 +19,8 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\GreaterThan; -use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -36,6 +33,11 @@ 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 */ @@ -49,6 +51,7 @@ public function __construct(TranslatorInterface $translator) public function buildForm(FormBuilderInterface $builder, array $options): void { $delayChoices = array_combine(self::DELAY_CHOICES, self::DELAY_CHOICES); + $abandonedCartChoices = array_combine(self::ABANDONED_CART_CHOICES, self::ABANDONED_CART_CHOICES); $builder ->add('DASHACTIVITY_CART_ACTIVE', ChoiceType::class, [ @@ -61,15 +64,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void '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', IntegerType::class, [ + ->add('DASHACTIVITY_CART_ABANDONED_MIN', ChoiceType::class, [ 'label' => $this->translator->trans('Abandoned cart (min)', [], self::DOMAIN), - 'help' => $this->translator->trans('How long (in hours) after the last action a cart is to be considered as abandoned (default: 24 hrs).', [], self::DOMAIN), - 'constraints' => [new NotBlank(), new GreaterThan(0)], + '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', IntegerType::class, [ + ->add('DASHACTIVITY_CART_ABANDONED_MAX', ChoiceType::class, [ 'label' => $this->translator->trans('Abandoned cart (max)', [], self::DOMAIN), - 'help' => $this->translator->trans('How long (in hours) after the last action a cart is no longer to be considered as abandoned (default: 24 hrs).', [], self::DOMAIN), - 'constraints' => [new NotBlank(), new GreaterThan(0)], + '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, ]) ; } diff --git a/upgrade/upgrade-2.2.0.php b/upgrade/upgrade-3.0.0.php similarity index 96% rename from upgrade/upgrade-2.2.0.php rename to upgrade/upgrade-3.0.0.php index c09177b..53c1010 100644 --- a/upgrade/upgrade-2.2.0.php +++ b/upgrade/upgrade-3.0.0.php @@ -27,7 +27,7 @@ exit; } -function upgrade_module_2_2_0($object) +function upgrade_module_3_0_0($object) { return $object->createConfigurationTab() && $object->registerHook('displayAdminDashboardZoneOne'); From 0a8b3a80cc47e415962b9cb71c472920076986a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Mon, 28 Sep 2026 14:08:09 +0200 Subject: [PATCH 7/7] Correct the legacy abandoned cart default and show hour equivalents in the delay choices --- src/Controller/ConfigurationController.php | 15 +++++++++++++++ src/Type/ConfigurationType.php | 20 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Controller/ConfigurationController.php b/src/Controller/ConfigurationController.php index ac2c8cb..8b2e5b7 100644 --- a/src/Controller/ConfigurationController.php +++ b/src/Controller/ConfigurationController.php @@ -35,6 +35,12 @@ class ConfigurationController extends FrameworkBundleAdminController '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'))") */ @@ -45,6 +51,15 @@ public function indexAction(Request $request): Response $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); diff --git a/src/Type/ConfigurationType.php b/src/Type/ConfigurationType.php index c4654a4..89319f3 100644 --- a/src/Type/ConfigurationType.php +++ b/src/Type/ConfigurationType.php @@ -50,8 +50,14 @@ public function __construct(TranslatorInterface $translator) public function buildForm(FormBuilderInterface $builder, array $options): void { - $delayChoices = array_combine(self::DELAY_CHOICES, self::DELAY_CHOICES); - $abandonedCartChoices = array_combine(self::ABANDONED_CART_CHOICES, self::ABANDONED_CART_CHOICES); + $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, [ @@ -84,4 +90,14 @@ public function configureOptions(OptionsResolver $resolver): void '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); + } }