diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 00000000..83c00b77
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,10 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(head:*)",
+ "Bash(cp:*)",
+ "Bash(cd:*)",
+ "Bash(curl:*)"
+ ]
+ }
+}
diff --git a/.vscode/ftp-kr.json b/.vscode/ftp-kr.json
index 90d36ac2..45cc6ca8 100644
--- a/.vscode/ftp-kr.json
+++ b/.vscode/ftp-kr.json
@@ -12,6 +12,7 @@
"ignoreRemoteModification": true,
"ignore": [
".git",
- "/.vscode"
+ "/.vscode",
+ "/.claude"
]
}
\ No newline at end of file
diff --git a/modules/customfeaturetab/classes/CustomFeatureTabRule.php b/modules/customfeaturetab/classes/CustomFeatureTabRule.php
new file mode 100644
index 00000000..a118d4d9
--- /dev/null
+++ b/modules/customfeaturetab/classes/CustomFeatureTabRule.php
@@ -0,0 +1,99 @@
+
+ * @copyright Project-Pro
+ * @license Proprietary - paid license
+ */
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+class CustomFeatureTabRule extends ObjectModel
+{
+ public $id_feature;
+ public $id_feature_value;
+ public $position;
+ public $active;
+ public $title;
+ public $content;
+ public $date_add;
+ public $date_upd;
+
+ public static $definition = array(
+ 'table' => 'custom_feature_tab',
+ 'primary' => 'id_custom_feature_tab',
+ 'multilang' => true,
+ 'fields' => array(
+ 'id_feature' => array(
+ 'type' => self::TYPE_INT,
+ 'validate' => 'isUnsignedId',
+ 'required' => true,
+ ),
+ 'id_feature_value' => array(
+ 'type' => self::TYPE_INT,
+ 'validate' => 'isUnsignedId',
+ 'required' => true,
+ ),
+ 'position' => array(
+ 'type' => self::TYPE_INT,
+ 'validate' => 'isUnsignedInt',
+ ),
+ 'active' => array(
+ 'type' => self::TYPE_BOOL,
+ 'validate' => 'isBool',
+ ),
+ 'date_add' => array(
+ 'type' => self::TYPE_DATE,
+ 'validate' => 'isDate',
+ ),
+ 'date_upd' => array(
+ 'type' => self::TYPE_DATE,
+ 'validate' => 'isDate',
+ ),
+ // Lang fields
+ 'title' => array(
+ 'type' => self::TYPE_STRING,
+ 'lang' => true,
+ 'validate' => 'isGenericName',
+ 'required' => true,
+ 'size' => 255,
+ ),
+ 'content' => array(
+ 'type' => self::TYPE_HTML,
+ 'lang' => true,
+ 'validate' => 'isCleanHtml',
+ ),
+ ),
+ );
+
+ /**
+ * Get active rules matching product features.
+ *
+ * @param array $productFeatures Array from Product::getFeaturesStatic
+ * @param int $idLang Language ID
+ * @return array
+ */
+ public static function getMatchingRules(array $productFeatures, $idLang)
+ {
+ if (empty($productFeatures)) {
+ return array();
+ }
+
+ $conditions = array();
+ foreach ($productFeatures as $feat) {
+ $conditions[] = '(' . (int) $feat['id_feature'] . ', ' . (int) $feat['id_feature_value'] . ')';
+ }
+
+ $sql = 'SELECT cft.*, cftl.`title`, cftl.`content`
+ FROM `' . _DB_PREFIX_ . 'custom_feature_tab` cft
+ LEFT JOIN `' . _DB_PREFIX_ . 'custom_feature_tab_lang` cftl
+ ON cft.`id_custom_feature_tab` = cftl.`id_custom_feature_tab`
+ AND cftl.`id_lang` = ' . (int) $idLang . '
+ WHERE cft.`active` = 1
+ AND (cft.`id_feature`, cft.`id_feature_value`) IN (' . implode(',', $conditions) . ')
+ ORDER BY cft.`position` ASC, cft.`id_custom_feature_tab` ASC';
+
+ return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
+ }
+}
diff --git a/modules/customfeaturetab/classes/index.php b/modules/customfeaturetab/classes/index.php
new file mode 100644
index 00000000..30c35169
--- /dev/null
+++ b/modules/customfeaturetab/classes/index.php
@@ -0,0 +1,8 @@
+
+
+ customfeaturetab
+
+
+
+
+ front_office_features
+ 1
+ 0
+
+
diff --git a/modules/customfeaturetab/controllers/admin/AdminCustomFeatureTabController.php b/modules/customfeaturetab/controllers/admin/AdminCustomFeatureTabController.php
new file mode 100644
index 00000000..a6a21981
--- /dev/null
+++ b/modules/customfeaturetab/controllers/admin/AdminCustomFeatureTabController.php
@@ -0,0 +1,246 @@
+
+ * @copyright Project-Pro
+ * @license Proprietary - paid license
+ */
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+require_once _PS_MODULE_DIR_ . 'customfeaturetab/classes/CustomFeatureTabRule.php';
+
+class AdminCustomFeatureTabController extends ModuleAdminController
+{
+ public function __construct()
+ {
+ $this->table = 'custom_feature_tab';
+ $this->className = 'CustomFeatureTabRule';
+ $this->identifier = 'id_custom_feature_tab';
+ $this->lang = true;
+ $this->bootstrap = true;
+ $this->addRowAction('edit');
+ $this->addRowAction('delete');
+ $this->position_identifier = 'position';
+
+ parent::__construct();
+
+ $this->bulk_actions = array(
+ 'delete' => array(
+ 'text' => $this->l('Delete selected'),
+ 'confirm' => $this->l('Delete selected items?'),
+ 'icon' => 'icon-trash',
+ ),
+ );
+
+ $this->fields_list = array(
+ 'id_custom_feature_tab' => array(
+ 'title' => $this->l('ID'),
+ 'align' => 'center',
+ 'class' => 'fixed-width-xs',
+ ),
+ 'feature_name' => array(
+ 'title' => $this->l('Cecha'),
+ 'filter_key' => 'fl!name',
+ ),
+ 'feature_value_name' => array(
+ 'title' => $this->l('Wartość cechy'),
+ 'filter_key' => 'fvl!value',
+ ),
+ 'title' => array(
+ 'title' => $this->l('Tytuł zakładki'),
+ 'filter_key' => 'b!title',
+ ),
+ 'position' => array(
+ 'title' => $this->l('Pozycja'),
+ 'align' => 'center',
+ 'class' => 'fixed-width-xs',
+ 'position' => 'position',
+ ),
+ 'active' => array(
+ 'title' => $this->l('Aktywna'),
+ 'active' => 'status',
+ 'type' => 'bool',
+ 'align' => 'center',
+ 'class' => 'fixed-width-sm',
+ ),
+ );
+ }
+
+ /**
+ * Override getList to JOIN feature/feature_value names.
+ */
+ public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false)
+ {
+ $this->_select = 'fl.`name` AS `feature_name`, fvl.`value` AS `feature_value_name`';
+ $this->_join = '
+ LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` fl
+ ON (a.`id_feature` = fl.`id_feature` AND fl.`id_lang` = ' . (int) $id_lang . ')
+ LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` fvl
+ ON (a.`id_feature_value` = fvl.`id_feature_value` AND fvl.`id_lang` = ' . (int) $id_lang . ')';
+
+ parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop);
+ }
+
+ /**
+ * Build the add/edit form.
+ */
+ public function renderForm()
+ {
+ $features = Feature::getFeatures($this->context->language->id);
+ $featureOptions = array();
+ foreach ($features as $feature) {
+ $featureOptions[] = array(
+ 'id' => $feature['id_feature'],
+ 'name' => $feature['name'],
+ );
+ }
+
+ // For edit mode, get values of the currently selected feature
+ $featureValueOptions = array();
+ $selectedFeatureValue = 0;
+ if ($this->object && $this->object->id) {
+ $selectedFeatureValue = (int) $this->object->id_feature_value;
+ $sql = 'SELECT fv.`id_feature_value`, fvl.`value`
+ FROM `' . _DB_PREFIX_ . 'feature_value` fv
+ LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` fvl
+ ON fv.`id_feature_value` = fvl.`id_feature_value`
+ AND fvl.`id_lang` = ' . (int) $this->context->language->id . '
+ WHERE fv.`id_feature` = ' . (int) $this->object->id_feature . '
+ ORDER BY fvl.`value` ASC';
+ $values = Db::getInstance()->executeS($sql);
+ if ($values) {
+ foreach ($values as $val) {
+ $featureValueOptions[] = array(
+ 'id' => $val['id_feature_value'],
+ 'name' => $val['value'],
+ );
+ }
+ }
+ }
+
+ $this->fields_form = array(
+ 'legend' => array(
+ 'title' => $this->l('Reguła karty cechy'),
+ 'icon' => 'icon-cogs',
+ ),
+ 'input' => array(
+ array(
+ 'type' => 'select',
+ 'label' => $this->l('Cecha'),
+ 'name' => 'id_feature',
+ 'required' => true,
+ 'options' => array(
+ 'query' => $featureOptions,
+ 'id' => 'id',
+ 'name' => 'name',
+ ),
+ ),
+ array(
+ 'type' => 'select',
+ 'label' => $this->l('Wartość cechy'),
+ 'name' => 'id_feature_value',
+ 'required' => true,
+ 'options' => array(
+ 'query' => $featureValueOptions,
+ 'id' => 'id',
+ 'name' => 'name',
+ ),
+ 'desc' => $this->l('Wartości zostaną załadowane po wyborze cechy.'),
+ ),
+ array(
+ 'type' => 'text',
+ 'label' => $this->l('Tytuł zakładki'),
+ 'name' => 'title',
+ 'lang' => true,
+ 'required' => true,
+ 'size' => 255,
+ ),
+ array(
+ 'type' => 'textarea',
+ 'label' => $this->l('Treść'),
+ 'name' => 'content',
+ 'lang' => true,
+ 'autoload_rte' => true,
+ 'rows' => 10,
+ 'cols' => 100,
+ ),
+ array(
+ 'type' => 'text',
+ 'label' => $this->l('Pozycja'),
+ 'name' => 'position',
+ 'class' => 'fixed-width-xs',
+ ),
+ array(
+ 'type' => 'switch',
+ 'label' => $this->l('Aktywna'),
+ 'name' => 'active',
+ 'is_bool' => true,
+ 'values' => array(
+ array('id' => 'active_on', 'value' => 1, 'label' => $this->l('Tak')),
+ array('id' => 'active_off', 'value' => 0, 'label' => $this->l('Nie')),
+ ),
+ ),
+ ),
+ 'submit' => array(
+ 'title' => $this->l('Zapisz'),
+ ),
+ );
+
+ return parent::renderForm();
+ }
+
+ /**
+ * Add JS for AJAX dropdown and pass the selected value.
+ */
+ public function setMedia($isNewTheme = false)
+ {
+ parent::setMedia($isNewTheme);
+
+ if (in_array($this->display, array('add', 'edit'))) {
+ $this->addJS(_PS_MODULE_DIR_ . 'customfeaturetab/views/js/admin.js');
+
+ // Pass the preselected feature value to JS for edit mode
+ if ($this->object && $this->object->id) {
+ Media::addJsDef(array(
+ 'customfeaturetab_selected_value' => (int) $this->object->id_feature_value,
+ ));
+ }
+ }
+ }
+
+ /**
+ * AJAX endpoint: get feature values for a given feature.
+ */
+ public function ajaxProcessGetFeatureValues()
+ {
+ $idFeature = (int) Tools::getValue('id_feature');
+ $idLang = (int) $this->context->language->id;
+
+ // Include ALL values (predefined + custom) for the feature
+ $sql = 'SELECT fv.`id_feature_value`, fvl.`value`
+ FROM `' . _DB_PREFIX_ . 'feature_value` fv
+ LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` fvl
+ ON fv.`id_feature_value` = fvl.`id_feature_value`
+ AND fvl.`id_lang` = ' . (int) $idLang . '
+ WHERE fv.`id_feature` = ' . (int) $idFeature . '
+ ORDER BY fvl.`value` ASC';
+
+ $values = Db::getInstance()->executeS($sql);
+ $result = array();
+ if ($values) {
+ foreach ($values as $val) {
+ $result[] = array(
+ 'id_feature_value' => $val['id_feature_value'],
+ 'value' => $val['value'],
+ );
+ }
+ }
+
+ header('Content-Type: application/json');
+ die(json_encode($result));
+ }
+}
diff --git a/modules/customfeaturetab/controllers/admin/index.php b/modules/customfeaturetab/controllers/admin/index.php
new file mode 100644
index 00000000..30c35169
--- /dev/null
+++ b/modules/customfeaturetab/controllers/admin/index.php
@@ -0,0 +1,8 @@
+
+ * @copyright Project-Pro
+ * @license Proprietary - paid license
+ */
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+require_once dirname(__FILE__) . '/classes/CustomFeatureTabRule.php';
+
+class CustomFeatureTab extends Module
+{
+ public function __construct()
+ {
+ $this->name = 'customfeaturetab';
+ $this->tab = 'front_office_features';
+ $this->version = '1.0.0';
+ $this->author = 'Project-Pro';
+ $this->need_instance = 0;
+ $this->bootstrap = true;
+
+ parent::__construct();
+
+ $this->displayName = $this->l('Karty cech produktu');
+ $this->description = $this->l('Dodaje dodatkowe zakładki na karcie produktu w zależności od przypisanych cech.');
+ $this->ps_versions_compliancy = array('min' => '1.7.0.0', 'max' => _PS_VERSION_);
+ }
+
+ public function install()
+ {
+ return parent::install()
+ && $this->installDb()
+ && $this->installTab()
+ && $this->registerHook('displayProductExtraContent');
+ }
+
+ public function uninstall()
+ {
+ return $this->uninstallDb()
+ && $this->uninstallTab()
+ && parent::uninstall();
+ }
+
+ private function installDb()
+ {
+ $sql = array();
+
+ $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'custom_feature_tab` (
+ `id_custom_feature_tab` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `id_feature` INT(11) UNSIGNED NOT NULL,
+ `id_feature_value` INT(11) UNSIGNED NOT NULL,
+ `position` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+ `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
+ `date_add` DATETIME NOT NULL,
+ `date_upd` DATETIME NOT NULL,
+ PRIMARY KEY (`id_custom_feature_tab`),
+ INDEX `idx_feature_value` (`id_feature`, `id_feature_value`)
+ ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;';
+
+ $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'custom_feature_tab_lang` (
+ `id_custom_feature_tab` INT(11) UNSIGNED NOT NULL,
+ `id_lang` INT(11) UNSIGNED NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `content` TEXT,
+ PRIMARY KEY (`id_custom_feature_tab`, `id_lang`)
+ ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;';
+
+ foreach ($sql as $query) {
+ if (!Db::getInstance()->execute($query)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private function uninstallDb()
+ {
+ return Db::getInstance()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'custom_feature_tab_lang`')
+ && Db::getInstance()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'custom_feature_tab`');
+ }
+
+ private function installTab()
+ {
+ $tab = new Tab();
+ $tab->class_name = 'AdminCustomFeatureTab';
+ $tab->module = $this->name;
+ $tab->id_parent = (int) Tab::getIdFromClassName('AdminCatalog');
+ $tab->icon = 'description';
+ $languages = Language::getLanguages(false);
+ foreach ($languages as $lang) {
+ $tab->name[$lang['id_lang']] = 'Karty cech produktu';
+ }
+
+ return $tab->add();
+ }
+
+ private function uninstallTab()
+ {
+ $idTab = (int) Tab::getIdFromClassName('AdminCustomFeatureTab');
+ if ($idTab) {
+ $tab = new Tab($idTab);
+ return $tab->delete();
+ }
+
+ return true;
+ }
+
+ public function getContent()
+ {
+ Tools::redirectAdmin($this->context->link->getAdminLink('AdminCustomFeatureTab'));
+ }
+
+ /**
+ * Hook: displayProductExtraContent
+ * Returns extra tabs for products that match feature rules.
+ */
+ public function hookDisplayProductExtraContent($params)
+ {
+ $product = $params['product'];
+ $idLang = (int) $this->context->language->id;
+
+ $productFeatures = Product::getFeaturesStatic((int) $product->id);
+ if (empty($productFeatures)) {
+ return array();
+ }
+
+ $rules = CustomFeatureTabRule::getMatchingRules($productFeatures, $idLang);
+ if (empty($rules)) {
+ return array();
+ }
+
+ $tabs = array();
+ foreach ($rules as $rule) {
+ $extraContent = new PrestaShop\PrestaShop\Core\Product\ProductExtraContent();
+ $extraContent->setTitle($rule['title']);
+ $extraContent->setContent($rule['content']);
+ $tabs[] = $extraContent;
+ }
+
+ return $tabs;
+ }
+}
diff --git a/modules/customfeaturetab/dbquery_temp.php b/modules/customfeaturetab/dbquery_temp.php
new file mode 100644
index 00000000..391af72f
--- /dev/null
+++ b/modules/customfeaturetab/dbquery_temp.php
@@ -0,0 +1,23 @@
+executeS($sql);
+if ($results) {
+ foreach ($results as $row) {
+ echo $row['id_product'] . ' | ' . $row['product_name'] . ' | ' . $row['feature_name'] . ' -> ' . $row['feature_value'] . "\n";
+ }
+} else {
+ echo "Brak wynikow\n";
+}
diff --git a/modules/customfeaturetab/index.php b/modules/customfeaturetab/index.php
new file mode 100644
index 00000000..1e7bc3a7
--- /dev/null
+++ b/modules/customfeaturetab/index.php
@@ -0,0 +1,14 @@
+--');
+ return;
+ }
+
+ // Use the current page URL, append ajax params
+ var ajaxUrl = baseUrl + '&ajax=1&action=getFeatureValues&id_feature=' + idFeature;
+
+ $.ajax({
+ url: ajaxUrl,
+ type: 'GET',
+ dataType: 'json',
+ success: function (data) {
+ $valueSelect.empty();
+ if (data && data.length) {
+ $.each(data, function (i, item) {
+ var selected = (item.id_feature_value == preselectedValue) ? ' selected' : '';
+ $valueSelect.append(
+ ''
+ );
+ });
+ preselectedValue = null;
+ } else {
+ $valueSelect.append('');
+ }
+ },
+ error: function (xhr, status, error) {
+ console.error('[customfeaturetab] AJAX error:', status, error);
+ }
+ });
+ });
+
+ if ($featureSelect.val()) {
+ $featureSelect.trigger('change');
+ }
+});
diff --git a/modules/customfeaturetab/views/js/index.php b/modules/customfeaturetab/views/js/index.php
new file mode 100644
index 00000000..30c35169
--- /dev/null
+++ b/modules/customfeaturetab/views/js/index.php
@@ -0,0 +1,8 @@
+{l s='Product Details' d='Shop.Theme.Catalog'}
-
- {l s='Attachments' d='Shop.Theme.Catalog'}
-
{foreach from=$product.extraContent item=extra key=extraKey}
{$extra.title}
{/foreach}
+
+ {l s='Attachments' d='Shop.Theme.Catalog'}
+
Opinie
@@ -268,6 +268,12 @@
{include file='catalog/_partials/product-details.tpl'}
{/block}
+ {foreach from=$product.extraContent item=extra key=extraKey}
+
+ {/foreach}
+
{block name='product_attachments'}
{/block}
{/block}
-
- {foreach from=$product.extraContent item=extra key=extraKey}
-
- {/foreach}
{hook h='displayProductTabContent'}