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'} - {foreach from=$product.extraContent item=extra key=extraKey} {/foreach} +