Mit csinál?

A blokk a kategória oldal layout-jában helyezhető el Journal3 Dynamic Content modulként. Automatikusan:

  • Csak akkor jelenik meg, ha az aktuális kategóriának van legalább egy aktív, az adott store-hoz tartozó közvetlen alkategóriája.
  • Összegyűjti az összes közvetlen alkategória aktív, raktáron lévő termékeit – duplikátumok nélkül, ha egy termék több alkategóriában is szerepel.
  • Sorba rendezi őket: elsődlegesen eladásszám szerint csökkenően (order_product tábla összesítése), másodlagosan megtekintésszám szerint csökkenően (product.viewed).
  • Legfeljebb annyi terméket jelenít meg, amennyit az admin termékszám-limit beállítása enged (config_limit_admin).
  • A megjelenítéshez a Journal3 natív journal3/products template-jét használja – pontosan ugyanolyan termékkártyák jelennek meg, mint a kategória oldal fő terméklistájában, beleértve a hover képet, labeleket, extra gombokat, retina képeket és az akciós ár visszaszámlálót.
  • A grid/list nézetváltás automatikusan követi az oldal többi terméklistájának állapotát, mivel ugyanazt a main-products konténer struktúrát és CSS osztályokat használja, amelyekre a Journal3 JavaScript épít.

Hogyan működik?

  1. Az URL path GET paraméteréből (pl. ?path=20_24) az utolsó szegmens adja meg az aktuális kategória ID-ját.
  2. Lekérdezi az aktuális kategória közvetlen, aktív, store-hoz rendelt alkategóriáit. Ha nincs egy sem, a metódus üres stringet ad vissza – a blokk nem renderelődik.
  3. Egy fan-out mentes SQL lekérdezéssel összegyűjti az alkategóriák termékeit, és rendezi őket eladás majd megtekintés szerint. Csak engedélyezett, az aktuális store-hoz tartozó, elérhető dátumú, és raktáron lévő (quantity > 0) termékek kerülnek be.
  4. A Journal3 saját model_journal3_product->getProduct() metódusával lekéri a teljes termékadatot Journal formátumban – tartalmazza a second_image, special_date_end és egyéb Journal-specifikus mezőket is.
  5. Az SQL által meghatározott sorrendet visszaállítja, mivel a Journal modell nem garantálja a sorrend megőrzését.
  6. Journal3 service-ek segítségével formázza az adatokat: journal3_image (1x/2x retina képek), journal3_product_extras (labelek, extra gombok, CSS osztályok), journal3_url (SEO URL-ek).
  7. Minden terméket a $this->load->view('journal3/products', $data) hívással renderel – ez a Journal3 saját termékkártya template-je.
  8. Az eredményt egy main-products konténer div-be csomagolja, amelynek CSS osztályai a Journal3 globális beállításaiból (globalProductView, globalProductGridType) jönnek – pontosan mint a category.twig-ben.

SQL fan-out kezelése

Ha egy termék több alkategóriában is szerepel, és a product_to_category táblát közvetlenül joinolnánk az order_product táblával, az eladásszám N-szeresére növekedne. A lekérdezés ezt két subquery-vel kerüli el:

  • p2c subquery: SELECT DISTINCT product_id FROM product_to_category WHERE category_id IN (...) – minden termék egyszer szerepel, függetlenül attól, hány alkategóriában van.
  • os subquery: az order_product összesítése termék szinten történik meg a join előtt, így a p2c nem szorozza meg az összeget.

Követelmények

KomponensVerzióMegjegyzés
OpenCart3.0.x
Journal3 téma3.2.xjournal3_image, journal3_product_extras, journal3_url service-ek szükségesek
PHP7.x / 8.x

Telepítés

1. Fájl másolása

Másold a controller fájlt az OpenCart gyökérkönyvtárába, megőrizve a mappastruktúrát:

upload/
└── catalog/controller/journal3/
    └── subcategory_products.php catalog/controller/journal3/subcategory_products.php
ℹ️

Külön Twig template fájl nem szükséges. A controller a Journal3 beépített journal3/products view-ját használja, amely már a témával együtt telepítve van.


Admin beállítás – Dynamic Content elhelyezése

  1. Nyisd meg: Journal Admin → Layouts → [Kategória layout szerkesztése]
  2. Kattints az „Add Module" gombra, és válaszd a Dynamic Content modult.
  3. A „Controller" mezőbe írd be: journal3/subcategory_products
  4. Helyezd el a blokkot a kívánt pozícióban (javasolt: a fő terméklista fölé, a content_top zónába).
  5. Mentsd el a layout-ot.
⚠️

Fontos: A blokk kizárólag kategória oldalakon jelenik meg, ahol az URL tartalmaz path GET paramétert. Más oldalakon (főoldal, termékoldal stb.) a controller automatikusan üres stringet ad vissza, a blokk nem renderelődik.


Termékszám limit

A megjelenített termékek maximális száma az OpenCart admin „Limit (Admin)" beállításából jön:

$limit = (int)$this->config->get('config_limit_admin');
if ($limit < 1) { $limit = 12; }

Beállítás helye: Admin → System → Settings → [Store] → Option fül → „Limit (Admin)". Ha az érték nulla vagy üres, a fallback értéke 12.

ℹ️

A Dynamic Content modulnak nincs saját per-instance limit beállítása a Journal admin UI-ban. Ha az itt megadott értéktől eltérő limitet szeretnél, módosítsd a $limit változó értékét közvetlenül a controller fájlban.


Fájlszerkezet

subcategory_products/
├── subcategory_products.html Ez a dokumentáció
└── upload/catalog/controller/journal3/
    └── subcategory_products.php Dynamic Content controller: SQL, adatformázás, Journal3 renderelés

Teljes kód

<?php

class ControllerJournal3SubcategoryProducts extends Controller {

    public function index($args = array()) {

        // 1. Aktuális kategória azonosítása
        $path = isset($this->request->get['path']) ? (string)$this->request->get['path'] : '';
        if (empty($path)) { return ''; }

        $parts       = explode('_', $path);
        $category_id = (int)end($parts);
        if ($category_id < 1) { return ''; }

        // 2. Konfiguráció
        $db       = $this->db;
        $store_id = (int)$this->config->get('config_store_id');
        $limit    = (int)$this->config->get('config_limit_admin');
        if ($limit < 1) { $limit = 12; }

        // 3. Közvetlen alkategóriák – ha nincs, a blokk nem jelenik meg
        $sub_result = $db->query("
            SELECT c.category_id
            FROM `" . DB_PREFIX . "category` c
            INNER JOIN `" . DB_PREFIX . "category_to_store` c2s
                   ON c2s.category_id = c.category_id
            WHERE  c.parent_id  = '" . (int)$category_id . "'
              AND  c.status     = '1'
              AND  c2s.store_id = '" . $store_id . "'"
        );
        if (!$sub_result->num_rows) { return ''; }

        $sub_ids_sql = implode(',', array_map('intval', array_column($sub_result->rows, 'category_id')));

        // 4. Termékek: eladásszám DESC → megtekintés DESC (fan-out mentes)
        $prod_result = $db->query("
            SELECT p.product_id,
                   COALESCE(os.total_sold, 0) AS total_sold, p.viewed
            FROM `" . DB_PREFIX . "product` p
            INNER JOIN (
                SELECT DISTINCT product_id FROM `" . DB_PREFIX . "product_to_category`
                WHERE category_id IN (" . $sub_ids_sql . ")"
            ") p2c ON p2c.product_id = p.product_id
            INNER JOIN `" . DB_PREFIX . "product_to_store` p2s
                   ON p2s.product_id = p.product_id AND p2s.store_id = '" . $store_id . "'
            LEFT JOIN (
                SELECT product_id, SUM(quantity) AS total_sold
                FROM `" . DB_PREFIX . "order_product` GROUP BY product_id
            ) os ON os.product_id = p.product_id
            WHERE p.status = '1'
              AND p.quantity > 0
              AND (p.date_available = '0000-00-00' OR p.date_available <= NOW())
            ORDER BY total_sold DESC, p.viewed DESC
            LIMIT " . $limit
        );
        if (!$prod_result->num_rows) { return ''; }

        $product_ids = array_column($prod_result->rows, 'product_id');

        // 5. Journal termékmodell + sorrend visszaállítása
        $this->load->model('journal3/product');
        $this->load->language('product/product');
        $raw = $this->model_journal3_product->getProduct($product_ids);
        if (empty($raw)) { return ''; }

        $by_id = array();
        foreach ($raw as $r) { $by_id[$r['product_id']] = $r; }
        $results = array();
        foreach ($product_ids as $id) {
            if (isset($by_id[$id])) { $results[] = $by_id[$id]; }
        }

        // 6. Journal megjelenítési beállítások
        $display   = $this->journal3->get('globalProductView')    ?: 'grid';
        $grid_type = $this->journal3->get('globalProductGridType') ?: 'ipr';
        $img_w     = (int)($this->config->get('theme_journal3_image_product_width')  ?: 240);
        $img_h     = (int)($this->config->get('theme_journal3_image_product_height') ?: 240);
        $img_r     = 'crop';

        $settings = array(
            'image_width'  => $img_w,  'image_height' => $img_h,  'image_resize' => $img_r,
            'swiper_carousel'                    => false,
            'products_classes'                   => array('product-' . $display),
            'moduleProductDescriptionLimit'      => 160,
            'moduleProductGridSecondImageStatus' => false,
            'moduleProductStat1'                 => '',
            'moduleProductStat2'                 => '',
            'button_cart'    => $this->language->get('button_cart'),
            'button_wishlist' => $this->language->get('button_wishlist'),
            'button_compare' => $this->language->get('button_compare'),
            'text_tax'       => $this->language->get('text_tax'),
        );

        // 7. Formázás + renderelés Journal journal3/products view-val
        $rendered = '';
        foreach ($results as $result) {
            $src     = $result['image'] ?: $this->journal3->get('placeholder');
            $thumb   = $this->journal3_image->resize($src, $img_w,   $img_h,   $img_r);
            $thumb2x = $this->journal3_image->resize($src, $img_w*2, $img_h*2, $img_r);

            $second_thumb = $second_thumb2x = false;
            if ($this->journal3->is_desktop
                && $this->journal3->get('globalProductGridSecondImageStatus')
                && !empty($result['second_image'])) {
                $second_thumb   = $this->journal3_image->resize($result['second_image'], $img_w,   $img_h,   $img_r);
                $second_thumb2x = $this->journal3_image->resize($result['second_image'], $img_w*2, $img_h*2, $img_r);
            }

            $price = ($this->customer->isLogged() || !$this->config->get('config_customer_price'))
                ? $this->currency->format($this->tax->calculate($result['price'], $result['tax_class_id'], $this->config->get('config_tax')), $this->session->data['currency'])
                : false;

            $special = false;
            if (!is_null($result['special']) && (float)$result['special'] >= 0) {
                $special = ($result['special_date_end'] && strtotime($result['special_date_end']) < time())
                    ? false
                    : $this->currency->format($this->tax->calculate($result['special'], $result['tax_class_id'], $this->config->get('config_tax')), $this->session->data['currency']);
            }

            $tax    = $this->config->get('config_tax') ? $this->currency->format((float)($special ? $result['special'] : $result['price']), $this->session->data['currency']) : false;
            $rating = $this->config->get('config_review_status') ? $result['rating'] : false;

            $classes       = $this->journal3_product_extras->exclude_button($result);
            $labels        = $this->journal3_product_extras->labels($result);
            $extra_buttons = $this->journal3_product_extras->extra_buttons($result);
            $classes['out-of-stock']     = $result['quantity'] <= 0;
            $classes['has-special']      = (bool)$special;
            $classes['has-countdown']    = $special && $result['special_date_end'];
            $classes['has-extra-button'] = (bool)$extra_buttons;
            $classes['has-zero-price']   = ($special ? (float)$result['special'] : (float)$result['price']) <= 0;

            $product_data = array(
                'product_id'     => $result['product_id'],
                'name'           => $result['name'],
                'description'    => mb_substr(trim(strip_tags(html_entity_decode($result['description'], ENT_QUOTES, 'UTF-8'))), 0, 160, 'UTF-8') . '..',
                'price'          => $price,         'special'   => $special,
                'tax'            => $tax,           'rating'    => $rating,
                'minimum'        => $result['minimum'] > 0 ? $result['minimum'] : 1,
                'href'           => $this->journal3_url->link('product/product', 'product_id=' . $result['product_id']),
                'thumb'          => $thumb,         'thumb2x'        => $thumb2x,
                'second_thumb'   => $second_thumb,  'second_thumb2x' => $second_thumb2x,
                'classes'        => $classes,       'labels'         => $labels,
                'extra_buttons'  => $extra_buttons, 'quantity'       => $result['quantity'],
                'stock_status'   => $result['stock_status'],
                'date_end'       => $result['special_date_end'],
                'price_value'    => ($special ? $result['special'] > 0 : $result['price'] > 0),
                'stat1'          => $this->journal3_product_extras->stat($result, ''),
                'stat2'          => $this->journal3_product_extras->stat($result, ''),
                'qid'            => uniqid('q-'),
                'button_cart'    => $this->journal3->get('filterAddToCartStock') && $result['quantity'] <= 0
                                    ? $result['stock_status']
                                    : $this->language->get('button_cart'),
            );

            $rendered .= $this->load->view('journal3/products', array_merge($settings, ['product' => $product_data]));
        }

        if (empty($rendered)) { return ''; }

        // 8. Konténer – category.twig main-products struktúrája
        return '<div class="main-products main-products-style product-'
             . $display . ' ' . $grid_type . '-grid">' . $rendered . '</div>';
    }
}

class_alias('ControllerJournal3SubcategoryProducts', '\Opencart\Catalog\Controller\Journal3\SubcategoryProducts');

Ismert problémák és megjegyzések

Termékszám limit nem konfigurálható Journal adminból

A Dynamic Content blokkoknak nincs per-instance beállítási lehetőségük a Journal admin UI-ban. A limit értéke az OpenCart Admin → System → Settings → Option → „Limit (Admin)" mezőjéből jön (config_limit_admin). Ha eltérő értéket szeretnél, módosítsd közvetlenül a controllerben a $limit változót.

Eladásszám minden rendelési státuszt tartalmaz

Az eladásszám az order_product tábla összes rekordját összesíti, státusztól függetlenül. Ha csak a teljesített rendeléseket szeretnéd figyelembe venni, az os subquery-t bővíteni kell egy INNER JOIN oc_order joinnal és megfelelő státusz szűrővel.

stat1 / stat2 üres értékkel hívódik

A Journal3 Products modulban a stat1 és stat2 értéke egy admin-konfigurált mező neve (pl. 'reviews', 'sold'). Mivel a Dynamic Content blokknak nincs ilyen konfigurációja, mindkét stat üres stringgel kerül átadásra – a stat mezők nem jelennek meg. Ha szükséges, módosítsd közvetlenül a controllerben.

⚠️

Journal3 frissítés után: A controller a journal3/products beépített Journal template-et használja. Ha egy Journal frissítés megváltoztatja ennek a template-nek az elvárt adatstruktúráját, a blokk megjelenése változhat. Frissítés után ellenőrizd a blokk megjelenését.

ℹ️

Frissítésállóság: A controller fájl a catalog/controller/journal3/ mappában van. A Journal3 frissítései nem írják felül az itt lévő egyedi fájlokat – csak a Journal saját fájljait frissítik.