first commit
This commit is contained in:
472
productcountdown.php
Normal file
472
productcountdown.php
Normal file
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncode;
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ProductCountdown extends Module
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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 = '
|
||||
<div class="row">
|
||||
<div class="col-lg-9">' . $formHtml . '</div>
|
||||
<div class="col-lg-3">' . $supportPanelHtml . '</div>
|
||||
</div>
|
||||
';
|
||||
|
||||
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 '<script>' . json_encode($method) . '</script>';
|
||||
|
||||
return $this->renderCountdown($args[0]);
|
||||
}
|
||||
}
|
||||
return parent::__call($method, $args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user