first commit

This commit is contained in:
O K
2025-09-07 12:50:02 +03:00
commit ba8c1e6957
10 changed files with 843 additions and 0 deletions

472
productcountdown.php Normal file
View 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);
}
}