name = 'productcountdown'; $this->tab = 'front_office_features'; $this->version = '1.0.0'; $this->author = 'Panariga'; $this->need_instance = 0; $this->ps_versions_compliancy = ['min' => '1.7.8', 'max' => _PS_VERSION_]; $this->bootstrap = true; parent::__construct(); $this->displayName = $this->trans('Product Discount Countdown', [], 'Modules.Productcountdown.Admin'); $this->description = $this->trans('Displays a countdown for products with a limited-time discount.', [], 'Modules.Productcountdown.Admin'); } public function isUsingNewTranslationSystem() { return true; } public function install() { if (!parent::install()) { return false; } // Set default configuration values Configuration::updateValue('PDC_ENABLED', 1); Configuration::updateValue('PDC_MAX_DAYS', 30); // Default hook is 'displayProductPriceBlock', but we register all of them. Configuration::updateValue('PDC_HOOK_POSITION', 'displayProductPriceBlock'); Configuration::updateValue('PDC_PREFIX_TEXT', ['en' => 'Special offer ends in:', 'fr' => 'Offre spéciale se termine dans :']); Configuration::updateValue('PDC_SHOW_NAME', 0); Configuration::updateValue('PDC_ON_EXPIRE', 'hide'); Configuration::updateValue('PDC_EXPIRED_TEXT', ['en' => 'This special offer has expired.']); Configuration::updateValue('PDC_BADGE_BG_COLOR', '#D9534F'); // A nice default red Configuration::updateValue('PDC_BADGE_TEXT_COLOR', '#FFFFFF'); // White text Configuration::updateValue('PDC_CUSTOM_CSS', ''); // Register ALL possible hooks. The logic to display will be inside each hook method. return $this->registerHook('displayHeader') && $this->registerHook('displayProductPriceBlock') && $this->registerHook('displayProductActions') && $this->registerHook('displayProductAdditionalInfo'); } // ... uninstall() remains the same ... public function uninstall() { // Delete all configuration values $configKeys = [ 'PDC_ENABLED', 'PDC_MAX_DAYS', 'PDC_HOOK_POSITION', 'PDC_PREFIX_TEXT', 'PDC_SHOW_NAME', 'PDC_ON_EXPIRE', 'PDC_EXPIRED_TEXT', 'PDC_CUSTOM_CSS', 'PDC_BADGE_BG_COLOR', 'PDC_BADGE_TEXT_COLOR' ]; foreach ($configKeys as $key) { Configuration::deleteByName($key); } return parent::uninstall(); } public function getContent() { $output = ''; if (Tools::isSubmit('submit' . $this->name)) { $this->postProcess(); $output .= $this->displayConfirmation($this->trans('Settings updated', [], 'Admin.Notifications.Success')); } // Generate the HTML for the form and the support panel $formHtml = $this->renderForm(); $supportPanelHtml = $this->renderSupportPanel(); // Combine them into a two-column layout using Bootstrap's grid system $finalHtml = '
' . $formHtml . '
' . $supportPanelHtml . '
'; return $output . $finalHtml; } /** * SIMPLIFIED postProcess() method */ protected function postProcess() { $form_values = $this->getConfigFormValues(); $languages = Language::getLanguages(false); foreach (array_keys($form_values) as $key) { if (in_array($key, ['PDC_PREFIX_TEXT', 'PDC_EXPIRED_TEXT'])) { $values = []; foreach ($languages as $lang) { $values[$lang['id_lang']] = Tools::getValue($key . '_' . $lang['id_lang']); } Configuration::updateValue($key, $values); } else { // The complex hook registration logic is no longer needed here. // We just save the value. Configuration::updateValue($key, Tools::getValue($key)); } } } // ... renderForm(), getSettingsForm(), getStylingForm(), getConfigFormValues() remain the same ... // --- HOOKS AND FRONT OFFICE LOGIC --- /** * NEW: Dedicated hook method for displayProductPriceBlock * This correctly handles the 'type' parameter. */ public function hookDisplayProductPriceBlock($params) { // 1. Check if this is the hook selected by the user in the configuration. if (Configuration::get('PDC_HOOK_POSITION') === 'displayProductPriceBlock') { // 2. Check for the specific 'type' to prevent multiple displays. if (isset($params['type']) && $params['type'] === 'after_price') { return $this->renderCountdown($params); } } } /** * NEW: Dedicated hook method for displayProductActions */ public function hookDisplayProductActions($params) { if (Configuration::get('PDC_HOOK_POSITION') === 'displayProductActions') { return $this->renderCountdown($params); } } /** * NEW: Dedicated hook method for displayProductAdditionalInfo */ public function hookDisplayProductAdditionalInfo($params) { if (Configuration::get('PDC_HOOK_POSITION') === 'displayProductAdditionalInfo') { return $this->renderCountdown($params); } } // ... hookDisplayHeader(), renderCountdown(), and getActiveDiscount() remain the same ... public function hookDisplayHeader() { if ($this->context->controller instanceof ProductController) { $this->context->controller->registerJavascript( 'module-productcountdown-js', 'modules/' . $this->name . '/views/js/front.js', ['position' => 'bottom', 'priority' => 150] ); $this->context->controller->registerStylesheet( 'module-productcountdown-css', 'modules/' . $this->name . '/views/css/front.css', ['media' => 'all', 'priority' => 150] ); $customCss = Configuration::get('PDC_CUSTOM_CSS'); if (!empty($customCss)) { Media::addInlineStyle($customCss); } } } public function renderCountdown($params) { if (!(bool)Configuration::get('PDC_ENABLED')) { return; } $product = isset($params['product']) ? $params['product'] : null; if (!$product || !is_object($product) || !isset($product->id)) { return; } $activeDiscount = $this->getActiveDiscount($product); if (!$activeDiscount) { return; } $activeDiscountTime = strtotime($activeDiscount['to']); $maxDays = (int)Configuration::get('PDC_MAX_DAYS', null, null, null, 30); $maxTime = ($maxDays > 0) ? (time() + 60 * 60 * 24 * $maxDays) : 0; if ($activeDiscountTime > time() && ($maxTime === 0 || $activeDiscountTime < $maxTime)) { $discountName = null; if ((bool)Configuration::get('PDC_SHOW_NAME') && (int)$activeDiscount['id_specific_price_rule'] > 0) { $rule = new SpecificPriceRule($activeDiscount['id_specific_price_rule']); if (Validate::isLoadedObject($rule)) { $discountName = $rule->name[$this->context->language->id] ?? current($rule->name); } } $prefixText = Configuration::get('PDC_PREFIX_TEXT', $this->context->language->id, null, null, 'Special offer ends in:'); $expiredText = Configuration::get('PDC_EXPIRED_TEXT', $this->context->language->id, null, null, 'This special offer has expired.'); $this->context->smarty->assign([ 'countdown_timestamp' => $activeDiscountTime, 'countdown_prefix' => $prefixText, 'countdown_discount_name' => $discountName, 'countdown_on_expire_action' => Configuration::get('PDC_ON_EXPIRE', null, null, null, 'hide'), 'countdown_expired_text' => $expiredText, // NEW: Assign colors to Smarty variables 'countdown_bg_color' => Configuration::get('PDC_BADGE_BG_COLOR'), 'countdown_text_color' => Configuration::get('PDC_BADGE_TEXT_COLOR'), ]); return $this->display(__FILE__, 'views/templates/hook/countdown.tpl'); } } public function getActiveDiscount($product) { $id_country = (int)($this->context->country->id ?? Configuration::get('PS_COUNTRY_DEFAULT')); return SpecificPrice::getSpecificPrice( (int)$product->id, $this->context->shop->id, $this->context->currency->id, $id_country, $this->context->customer->id_default_group, 1, null, null ); } /** * Builds the configuration form using HelperForm. */ public function renderForm() { $helper = new HelperForm(); $helper->show_toolbar = false; $helper->table = $this->table; $helper->module = $this; $helper->default_form_language = $this->context->language->id; $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0); $helper->identifier = $this->identifier; $helper->submit_action = 'submit' . $this->name; $helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false) . '&configure=' . $this->name . '&tab_module=' . $this->tab . '&module_name=' . $this->name; $helper->token = Tools::getAdminTokenLite('AdminModules'); $helper->tpl_vars = [ 'fields_value' => $this->getConfigFormValues(), 'languages' => $this->context->controller->getLanguages(), 'id_language' => $this->context->language->id, ]; return $helper->generateForm([$this->getSettingsForm(), $this->getStylingForm()]); } /** * Defines the "Settings" section of the form. */ protected function getSettingsForm() { $hookOptions = [ ['id_option' => 'displayProductPriceBlock', 'name' => $this->trans('After Price (Recommended)', [], 'Modules.Productcountdown.Admin')], ['id_option' => 'displayProductActions', 'name' => $this->trans('Product Actions (Near Add to Cart)', [], 'Modules.Productcountdown.Admin')], ['id_option' => 'displayProductAdditionalInfo', 'name' => $this->trans('Product Additional Info (Tabs)', [], 'Modules.Productcountdown.Admin')], ]; $expireOptions = [ ['id_option' => 'hide', 'name' => $this->trans('Hide the countdown timer', [], 'Modules.Productcountdown.Admin')], ['id_option' => 'reload', 'name' => $this->trans('Reload the page', [], 'Modules.Productcountdown.Admin')], ['id_option' => 'message', 'name' => $this->trans('Show an "Expired" message', [], 'Modules.Productcountdown.Admin')], ]; return [ 'form' => [ 'legend' => [ 'title' => $this->trans('General Settings', [], 'Modules.Productcountdown.Admin'), 'icon' => 'icon-cogs', ], 'input' => [ [ 'type' => 'switch', 'label' => $this->trans('Enable Countdown', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_ENABLED', 'is_bool' => true, 'desc' => $this->trans('Globally enable or disable the countdown timer.', [], 'Modules.Productcountdown.Admin'), 'values' => [['id' => 'active_on', 'value' => 1, 'label' => $this->trans('Enabled', [], 'Admin.Global')], ['id' => 'active_off', 'value' => 0, 'label' => $this->trans('Disabled', [], 'Admin.Global')]], ], [ 'type' => 'text', 'label' => $this->trans('Display Threshold', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_MAX_DAYS', 'class' => 'fixed-width-sm', 'suffix' => $this->trans('days', [], 'Modules.Productcountdown.Admin'), 'desc' => $this->trans('Only show the timer if the discount expires within this many days. Set to 0 to always show.', [], 'Modules.Productcountdown.Admin'), ], [ 'type' => 'select', 'label' => $this->trans('Display Position (Hook)', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_HOOK_POSITION', 'options' => ['query' => $hookOptions, 'id' => 'id_option', 'name' => 'name'], 'desc' => $this->trans('Choose where the countdown timer should appear.', [], 'Modules.Productcountdown.Admin'), ], [ 'type' => 'text', 'label' => $this->trans('Countdown Prefix Text', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_PREFIX_TEXT', 'lang' => true, 'desc' => $this->trans('This text appears before the timer (e.g., "Offer ends in:").', [], 'Modules.Productcountdown.Admin'), ], [ 'type' => 'switch', 'label' => $this->trans('Display Discount Name', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_SHOW_NAME', 'is_bool' => true, 'desc' => $this->trans('If available, show the name of the Catalog Price Rule.', [], 'Modules.Productcountdown.Admin'), 'values' => [['id' => 'active_on', 'value' => 1, 'label' => $this->trans('Yes', [], 'Admin.Global')], ['id' => 'active_off', 'value' => 0, 'label' => $this->trans('No', [], 'Admin.Global')]], ], [ 'type' => 'select', 'label' => $this->trans('Action on Expiry', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_ON_EXPIRE', 'options' => ['query' => $expireOptions, 'id' => 'id_option', 'name' => 'name'], 'desc' => $this->trans('What should happen when the timer reaches zero?', [], 'Modules.Productcountdown.Admin'), ], [ 'type' => 'text', 'label' => $this->trans('Expired Message Text', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_EXPIRED_TEXT', 'lang' => true, 'desc' => $this->trans('This message is shown only if "Action on Expiry" is set to "Show an Expired message".', [], 'Modules.Productcountdown.Admin'), ], ], 'submit' => [ 'title' => $this->trans('Save', [], 'Admin.Actions'), ], ], ]; } /** * Renders the support and donation panel for the configuration page. */ protected function renderSupportPanel() { // Define your donation link here $donationLink = 'https://secure.wayforpay.com/donate/dd579282b23b4'; // <--- CHANGE THIS LINK $this->context->smarty->assign([ 'panel_title' => $this->trans('Enjoying this module?', [], 'Modules.Productcountdown.Admin'), 'panel_body_text_1' => $this->trans('This module is offered for free on GitHub.', [], 'Modules.Productcountdown.Admin'), 'panel_body_text_2' => $this->trans('If it helps your business, please consider supporting its future development by making a small donation.', [], 'Modules.Productcountdown.Admin'), 'button_text' => $this->trans('Buy me a donut', [], 'Modules.Productcountdown.Admin'), 'donation_link' => $donationLink, ]); return $this->display(__FILE__, 'views/templates/admin/support_panel.tpl'); } /** * Defines the "Styling" section of the form. */ protected function getStylingForm() { return [ 'form' => [ 'legend' => [ 'title' => $this->trans('Styling', [], 'Modules.Productcountdown.Admin'), 'icon' => 'icon-paint-brush', ], 'input' => [ // NEW: Color pickers [ 'type' => 'color', 'label' => $this->trans('Badge Background Color', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_BADGE_BG_COLOR', 'desc' => $this->trans('Choose the background color for the countdown badge.', [], 'Modules.Productcountdown.Admin'), ], [ 'type' => 'color', 'label' => $this->trans('Badge Text Color', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_BADGE_TEXT_COLOR', 'desc' => $this->trans('Choose the text color for the countdown badge.', [], 'Modules.Productcountdown.Admin'), ], // END NEW [ 'type' => 'textarea', 'label' => $this->trans('Custom CSS', [], 'Modules.Productcountdown.Admin'), 'name' => 'PDC_CUSTOM_CSS', 'desc' => $this->trans('Add your custom CSS rules here. The main container has the class ".product-countdown-container".', [], 'Modules.Productcountdown.Admin'), ], ], 'submit' => [ 'title' => $this->trans('Save', [], 'Admin.Actions'), ], ], ]; } /** * Retrieves current configuration values to populate the form. */ protected function getConfigFormValues() { $languages = Language::getLanguages(false); $fields = []; $fields['PDC_ENABLED'] = Configuration::get('PDC_ENABLED'); $fields['PDC_MAX_DAYS'] = Configuration::get('PDC_MAX_DAYS'); $fields['PDC_HOOK_POSITION'] = Configuration::get('PDC_HOOK_POSITION'); $fields['PDC_SHOW_NAME'] = Configuration::get('PDC_SHOW_NAME'); $fields['PDC_ON_EXPIRE'] = Configuration::get('PDC_ON_EXPIRE'); $fields['PDC_CUSTOM_CSS'] = Configuration::get('PDC_CUSTOM_CSS'); $fields['PDC_BADGE_BG_COLOR'] = Configuration::get('PDC_BADGE_BG_COLOR'); $fields['PDC_BADGE_TEXT_COLOR'] = Configuration::get('PDC_BADGE_TEXT_COLOR'); foreach ($languages as $lang) { $fields['PDC_PREFIX_TEXT'][$lang['id_lang']] = Tools::getValue( 'PDC_PREFIX_TEXT_' . $lang['id_lang'], Configuration::get('PDC_PREFIX_TEXT', $lang['id_lang']) ); $fields['PDC_EXPIRED_TEXT'][$lang['id_lang']] = Tools::getValue( 'PDC_EXPIRED_TEXT_' . $lang['id_lang'], Configuration::get('PDC_EXPIRED_TEXT', $lang['id_lang']) ); } return $fields; } // --- HOOKS AND FRONT OFFICE LOGIC --- public function __call($method, $args) { if (strpos($method, 'hook') === 0) { $configuredHook = Configuration::get('PDC_HOOK_POSITION', null, null, null, 'displayProductPriceBlock'); if (strtolower($method) === 'hook' . strtolower($configuredHook)) { echo ''; return $this->renderCountdown($args[0]); } } return parent::__call($method, $args); } }