Вывод полей в таблице заказов minishop2

Приветствую!
Столкнулся с задачей изменить таблицу заказов minishop2.
Но на просторах интернета, так и не нашел подобного решения.
Существуют внятные и понятные (даже для моего уровня программирования) статьи о том как добавить новое поле, к примеру в закладку адрес, но вот по работе с самой таблицей, информации очень мало.

Если более детально описать, то требуется в таблицу с заказами вывести любое поле из таблицы ms2_order_addresses.

У меня получилось добавить новые колонки в таблице, но они пусты. На сколько я понимаю, в момент генерации таблицы нет запроса к ms2_order_addresses, сама таблица работает с ms2_orders.



На скрине колонка «Адрес получателя» живет аккурат в таблице ms2_order_addresses и пуста.

Буду благодарен помощи!

Обновление 19.03.2025
Благодаря персту указующему, в лице Евгений Webinmd я смог разобраться.

Немного забегая вперед, данный текст я пишу из двух умыслов.
Во-первых, я скромно надеюсь, что кому-то это будет полезно, а во-вторых, будет здорово, если кто-то укажет мне на мои ошибки и недочеты, т.к. мой уровень программирования, ограничивается if else и echo «Hello, world!»; =))) А если уж совсем на чистоту, из всего кода, который я буду приводить ниже, я 70% не понимаю, от слова совсем!

И так, приступим!
Действительно за вывод колонок в таблице с заказами отвечает процессор getlist.
Его можно найти core/components/minishop2/processors/mgr/
так же, за вывод колонок отвечает файл orders.grid.js
он живет assets/components/minishop2/js/mgr/orders/orders.grid.js

Так же понадобится системная настройка ms2_order_grid_fields — в ней нужно дописывать необходимые нам поля.
И так. Теперь по порядку.

Опираясь на эту статью я родил следующее:

1. Я создал новый процессор getlist_custom.class.php и разместил его по адресу: core/components/minishop2/processors/mgr/orders/getlist_custom.class.php

<?php
require_once(dirname(__FILE__) . '/getlist.class.php');

class msOrderGetListProcessorCustom extends modObjectGetListProcessor
{
    public $classKey = 'msOrder';
    public $languageTopics = ['default', 'minishop2:manager'];
    public $defaultSortField = 'id';
    public $defaultSortDirection = 'DESC';
    public $permission = 'msorder_list';
    /** @var  miniShop2 $ms2 */
    protected $ms2;
    /** @var  xPDOQuery $query */
    protected $query;

    /**
     * @return bool|null|string
     */
    public function initialize()
    {
        $this->ms2 = $this->modx->getService('miniShop2');

        if (!$this->modx->hasPermission($this->permission)) {
            return $this->modx->lexicon('access_denied');
        }

        return parent::initialize();
    }

    /**
     * @param xPDOQuery $c
     *
     * @return xPDOQuery
     */
    public function prepareQueryBeforeCount(xPDOQuery $c)
    {
        $c->leftJoin('modUser', 'User');
        $c->leftJoin('modUserProfile', 'UserProfile');
        $c->leftJoin('msOrderStatus', 'Status');
        $c->leftJoin('msDelivery', 'Delivery');
        $c->leftJoin('msPayment', 'Payment');
        $c->leftJoin('msOrderAddress', 'Address');
        //$c->leftJoin('msOrderAddress', 'Region');

        $query = trim($this->getProperty('query'));
        if (!empty($query)) {
            if (is_numeric($query)) {
                $c->andCondition([
                    'id' => $query,
                    'OR:Address.phone:LIKE' => "%{$query}%",
                    //'OR:User.id' => $query,
                ]);
            } else {
                $c->where([
                    'num:LIKE' => "{$query}%",
                    'OR:order_comment:LIKE' => "%{$query}%",
                    'OR:Address.comment:LIKE' => "%{$query}%",
                    'OR:User.username:LIKE' => "%{$query}%",
                    'OR:UserProfile.fullname:LIKE' => "%{$query}%",
                    'OR:UserProfile.email:LIKE' => "%{$query}%",
                    'OR:Address.phone:LIKE' => "%{$query}%",
                ]);
            }
        }
        if ($status = $this->getProperty('status')) {
            $c->where([
                'status' => $status,
            ]);
        }
        if ($customer = $this->getProperty('customer')) {
            $c->where([
                'user_id' => (int)$customer,
            ]);
        }
        if ($context = $this->getProperty('context')) {
            $c->where([
                'context' => $context,
            ]);
        }
        if ($date_start = $this->getProperty('date_start')) {
            $c->andCondition([
                'createdon:>=' => date('Y-m-d 00:00:00', strtotime($date_start)),
            ], null, 1);
        }
        if ($date_end = $this->getProperty('date_end')) {
            $c->andCondition([
                'createdon:<=' => date('Y-m-d 23:59:59', strtotime($date_end)),
            ], null, 1);
        }


        $this->query = clone $c;

        $c->select(
            $this->modx->getSelectColumns('msOrder', 'msOrder', '', ['status', 'delivery', 'payment'], true) . ',
            msOrder.status as status_id, msOrder.delivery as delivery_id, msOrder.payment as payment_id,
            UserProfile.fullname as customer, User.username as customer_username,
            Address.region as region, Address.text_address as text_address, Address.city as city, Address.phone as phone,
            Status.name as status, Status.color, Delivery.name as delivery, Payment.name as payment'
        );
        $c->groupby($this->classKey . '.id');

        return $c;
    }

    /**
     * @param xPDOQuery $c
     *
     * @return xPDOQuery
     */
    public function prepareQueryAfterCount(xPDOQuery $c)
    {
        $total = 0;
        $limit = (int)$this->getProperty('limit');
        $start = (int)$this->getProperty('start');

        $q = clone $c;
        $q->query['columns'] = ['SQL_CALC_FOUND_ROWS msOrder.id, fullname as customer'];
        $sortClassKey = $this->getSortClassKey();
        $sortKey = $this->modx->getSelectColumns(
            $sortClassKey,
            $this->getProperty('sortAlias', $sortClassKey),
            '',
            [$this->getProperty('sort')]
        );
        if (empty($sortKey)) {
            $sortKey = $this->getProperty('sort');
        }
        $q->sortby($sortKey, $this->getProperty('dir'));
        if ($limit > 0) {
            $q->limit($limit, $start);
        }

        $ids = [];
        if ($q->prepare() and $q->stmt->execute()) {
            $ids = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
            $total = $this->modx->query('SELECT FOUND_ROWS()')->fetchColumn();
        }
        $ids = empty($ids) ? "(0)" : "(" . implode(',', $ids) . ")";
        $c->query['where'] = [
            [
                new xPDOQueryCondition(['sql' => 'msOrder.id IN ' . $ids, 'conjunction' => 'AND']),
            ]
        ];
        $c->sortby($sortKey, $this->getProperty('dir'));

        $this->setProperty('total', $total);

        return $c;
    }

    /**
     * @return array
     */
    public function getData()
    {
        $c = $this->modx->newQuery($this->classKey);
        $c = $this->prepareQueryBeforeCount($c);
        $c = $this->prepareQueryAfterCount($c);
        return [
            'results' => ($c->prepare() and $c->stmt->execute()) ? $c->stmt->fetchAll(PDO::FETCH_ASSOC) : [],
            'total' => (int)$this->getProperty('total'),
        ];
    }

    /**
     * @param array $data
     *
     * @return array
     */
    public function iterate(array $data)
    {
        $list = [];
        $list = $this->beforeIteration($list);
        $this->currentIndex = 0;
        /** @var xPDOObject|modAccessibleObject $object */
        foreach ($data['results'] as $array) {
            $list[] = $this->prepareArray($array);
            $this->currentIndex++;
        }
        return $this->afterIteration($list);
    }

    /**
     * @param array $data
     *
     * @return array
     */
    public function prepareArray(array $data)
    {
        if (empty($data['customer'])) {
            $data['customer'] = $data['customer_username'];
        }
        if (isset($data['cost'])) {
            $data['cost'] = $this->ms2->formatPrice($data['cost']);
        }
        if (isset($data['cart_cost'])) {
            $data['cart_cost'] = $this->ms2->formatPrice($data['cart_cost']);
        }
        if (isset($data['delivery_cost'])) {
            $data['delivery_cost'] = $this->ms2->formatPrice($data['delivery_cost']);
        }
        if (isset($data['weight'])) {
            $data['weight'] = $this->ms2->formatWeight($data['weight']);
        }
        if (isset($data['region'])) {
            //$data['customer'] = $data['customer_username'];
            $data['region'] = $data['region'];
        }
        if (isset($data['text_address'])) {
            $data['text_address'] = $data['text_address'];
        }
        if (isset($data['city'])) {
            $data['city'] = $data['city'];
        }
        if (isset($data['phone'])) {
            $data['phone'] = $data['phone'];
        }
        

        $data['actions'] = [
            [
                'cls' => '',
                'icon' => 'icon icon-edit',
                'title' => $this->modx->lexicon('ms2_menu_update'),
                'action' => 'updateOrder',
                'button' => true,
                'menu' => true,
            ],
            [
                'cls' => [
                    'menu' => 'red',
                    'button' => 'red',
                ],
                'icon' => 'icon icon-trash-o',
                'title' => $this->modx->lexicon('ms2_menu_remove'),
                'multiple' => $this->modx->lexicon('ms2_menu_remove_multiple'),
                'action' => 'removeOrder',
                'button' => true,
                'menu' => true,
            ],
            /*
            array(
                'cls' => '',
                'icon' => 'icon icon-cog actions-menu',
                'menu' => false,
                'button' => true,
                'action' => 'showMenu',
                'type' => 'menu',
            ),
            */
        ];

        return $data;
    }

    /**
     * @param array $array
     * @param bool $count
     *
     * @return string
     */
    public function outputArray(array $array, $count = false)
    {
        if ($count === false) {
            $count = count($array);
        }

        $selected = $this->query;
        $selected->query['columns'] = [];
        $selected->query['limit'] =
        $selected->query['offset'] = 0;
        $selected->where(['type' => 0]);
        $selected->select('SUM(msOrder.cost)');
        $selected->prepare();
        $selected->stmt->execute();

        $month = $this->modx->newQuery($this->classKey);
        $statuses = $this->modx->getOption('ms2_status_for_stat', null, '2,3');
        $statuses = array_map('trim', explode(',', $statuses));
        $month->where(['status:IN' => $statuses, 'type' => 0]);
        $month->where('createdon BETWEEN NOW() - INTERVAL 30 DAY AND NOW()');
        $month->select('SUM(msOrder.cost) as sum, COUNT(msOrder.id) as total');
        $month->prepare();
        $month->stmt->execute();
        $month = $month->stmt->fetch(PDO::FETCH_ASSOC);

        $data = [
            'success' => true,
            'results' => $array,
            'total' => $count,
            'num' => number_format($count, 0, '.', ' '),
            'sum' => number_format(round($selected->stmt->fetchColumn()), 0, '.', ' '),
            'month_sum' => number_format(round($month['sum']), 0, '.', ' '),
            'month_total' => number_format($month['total'], 0, '.', ' '),
        ];

        return json_encode($data);
    }
}

return 'msOrderGetListProcessorCustom';

По сути я просто скопировал оригинальный getlist.class.php, но внес некоторые изменения. А именно:

$c->select(
            $this->modx->getSelectColumns('msOrder', 'msOrder', '', ['status', 'delivery', 'payment'], true) . ',
            msOrder.status as status_id, msOrder.delivery as delivery_id, msOrder.payment as payment_id,
            UserProfile.fullname as customer, User.username as customer_username,
            Address.region as region, Address.text_address as text_address, Address.city as city, Address.phone as phone,
            Status.name as status, Status.color, Delivery.name as delivery, Payment.name as payment'
        );
строка — Address.region as region, Address.text_address as text_address, Address.city as city, Address.phone as phone,

Т.е. я дополнил запрос в БД нужными мне полями.

и вот здесь, тоже внес некоторые изменения:
if (empty($data['customer'])) {
            $data['customer'] = $data['customer_username'];
        }
        if (isset($data['cost'])) {
            $data['cost'] = $this->ms2->formatPrice($data['cost']);
        }
        if (isset($data['cart_cost'])) {
            $data['cart_cost'] = $this->ms2->formatPrice($data['cart_cost']);
        }
        if (isset($data['delivery_cost'])) {
            $data['delivery_cost'] = $this->ms2->formatPrice($data['delivery_cost']);
        }
        if (isset($data['weight'])) {
            $data['weight'] = $this->ms2->formatWeight($data['weight']);
        }
        if (isset($data['region'])) {
            //$data['customer'] = $data['customer_username'];
            $data['region'] = $data['region'];
        }
        if (isset($data['text_address'])) {
            $data['text_address'] = $data['text_address'];
        }
        if (isset($data['city'])) {
            $data['city'] = $data['city'];
        }
        if (isset($data['phone'])) {
            $data['phone'] = $data['phone'];
        }
Начиная со строки if (isset($data['region'])) и до конца

На сколько я понимаю, тут я добавляю в массив данных нужные мне значения.
Собственно это почти все изменения, которые я внес, осталось совсем чуть-чуть!

В самом начале файла
require_once(dirname(__FILE__) . '/getlist.class.php');
class msOrderGetListProcessor<strong>Custom</strong> extends modObjectGetListProcessor
Если еще вторую строчку я понимаю, для чего она нужна, то первую, там где идет подключение оригинального процессора — остается для меня загадкой, но без нее не работает =)

И в самом низу файла:
return 'msOrderGetListProcessorCustom';
Тут собственно надо в вывод указать, то что нужно возвращать уже не оригинал результата действия процессора, а измененный, тот, который я создал. Справедливости ради стоит сказать, что я не очень понимаю, что такое процессор, да и вообще как это все устроено. Буду рад если кто разъяснит.

Теперь настала очередь, создать кастомный файл с яваскриптами.
У меня он именуется orderGridChange.js и лежит assets/custom/orderGridChange.js

По сути, я скопировал файл из orders.grid.js и немного изменил его

if(Ext.ComponentMgr.types['minishop2-grid-orders']) {
miniShop2.grid.customOrders = function (config) {
    config = config || {};
    if (!config.id) {
        config.id = 'minishop2-grid-orders';
    }

    Ext.applyIf(config, {
        baseParams: {
            action: 'mgr/orders/getlist_custom',
            sort: 'id',
            dir: 'desc',
        },
        multi_select: true,
        changed: false,
        stateful: true,
        stateId: config.id,
    });
    miniShop2.grid.customOrders.superclass.constructor.call(this, config);
};


Ext.extend(miniShop2.grid.customOrders, Ext.ComponentMgr.types['minishop2-grid-orders'], {

    getFields: function () {
        return miniShop2.config['order_grid_fields'];
    },

    getColumns: function () {
        var all = {
            id: {width: 35},
            //customer: {width: 100, renderer: function (val, cell, row) {
            //    return miniShop2.utils.userLink(val, row.data['user_id'], true);
            //}},
            customer: {width: 50},
            //num: {width: 20},
            //receiver: {width: 100},
            //createdon: {width: 20, renderer: miniShop2.utils.formatDate},
            //updatedon: {width: 20, renderer: miniShop2.utils.formatDate},
            cost: {width: 50, renderer: this._renderCost},
            cart_cost: {width: 50},
            delivery_cost: {width: 75},
            weight: {width: 50},
            status: {width: 50, renderer: miniShop2.utils.renderBadge},
            delivery: {width: 75},
            order_comment: {width: 100},
            payment: {width: 75},
            text_address: {width: 150},
            city: {width: 100}, //получатель
            region: {width: 75}, //дата доставки
            phone: {width: 100},  //телефон заказчика
            //context: {width: 50},
            actions: {width: 75, id: 'actions', renderer: miniShop2.utils.renderActions, sortable: false},
        };

        var fields = this.getFields();
        var columns = [];
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            if (all[field]) {
                Ext.applyIf(all[field], {
                    header: _('ms2_' + field),
                    dataIndex: field,
                    sortable: true,
                });
                columns.push(all[field]);
            }
        }

        return columns;
    },

    getTopBar: function () {
        return [];
    },

    getListeners: function () {
        return {
            rowDblClick: function (grid, rowIndex, e) {
                var row = grid.store.getAt(rowIndex);
                this.updateOrder(grid, e, row);
            },
            afterrender: function (grid) {
                var params = miniShop2.utils.Hash.get();
                var order = params['order'] || '';
                if (order) {
                    this.updateOrder(grid, Ext.EventObject, {data: {id: order}});
                }
            },
        };
    },

    orderAction: function (method) {
        var ids = this._getSelectedIds();
        if (!ids.length) {
            return false;
        }
        MODx.Ajax.request({
            url: this.config.url,
            params: {
                action: 'mgr/orders/multiple',
                method: method,
                ids: Ext.util.JSON.encode(ids),
            },
            listeners: {
                success: {
                    fn: function () {
                        //noinspection JSUnresolvedFunction
                        this.refresh();
                    }, scope: this
                },
                failure: {
                    fn: function (response) {
                        MODx.msg.alert(_('error'), response.message);
                    }, scope: this
                },
            }
        })
    },

    updateOrder: function (btn, e, row) {
        if (typeof(row) != 'undefined') {
            this.menu.record = row.data;
        }
        var id = this.menu.record.id;

        MODx.Ajax.request({
            url: this.config.url,
            params: {
                action: 'mgr/orders/get',
                id: id
            },
            listeners: {
                success: {
                    fn: function ® {
                        var w = Ext.getCmp('minishop2-window-order-update');
                        if (w) {
                            w.close();
                        }

                        w = MODx.load({
                            xtype: 'minishop2-window-order-update',
                            id: 'minishop2-window-order-update',
                            record: r.object,
                            listeners: {
                                success: {
                                    fn: function () {
                                        this.refresh();
                                    }, scope: this
                                },
                                hide: {
                                    fn: function () {
                                        miniShop2.utils.Hash.remove('order');
                                        if (miniShop2.grid.Orders.changed === true) {
                                            Ext.getCmp('minishop2-grid-orders').getStore().reload();
                                            miniShop2.grid.Orders.changed = false;
                                        }
                                    }
                                },
                                afterrender: function () {
                                    miniShop2.utils.Hash.add('order', r.object['id']);
                                }
                            }
                        });
                        w.fp.getForm().reset();
                        w.fp.getForm().setValues(r.object);
                        w.show(e.target);
                    }, scope: this
                }
            }
        });
    },

    removeOrder: function () {
        var ids = this._getSelectedIds();

        Ext.MessageBox.confirm(
            _('ms2_menu_remove_title'),
            ids.length > 1
                ? _('ms2_menu_remove_multiple_confirm')
                : _('ms2_menu_remove_confirm'),
            function (val) {
                if (val == 'yes') {
                    this.orderAction('remove');
                }
            },
            this
        );
    },

    _renderCost: function (val, idx, rec) {
        return rec.data['type'] != undefined && rec.data['type'] == 1
            ? '-' + val
            : val;
    },

});
Ext.reg('minishop2-grid-orders', miniShop2.grid.customOrders);
}
а именно
if(Ext.ComponentMgr.types['minishop2-grid-orders']) {
Это самая первая строчка, которая создает условие на весь код, и закрывается скобка в самом конце файла, на эту скобку было потрачено около часа, т.к. я ее пропустил, не повторяйте мою ошибку=)))

В этой части
Ext.applyIf(config, {
        baseParams: {
            action: 'mgr/orders/getlist_custom',
            sort: 'id',
            dir: 'desc',
        },
я указал отсылку на новый процессор, строка
action: 'mgr/orders/getlist_custom',

Так же во всех местах где встречался объект (?) Orders, я его заменил на customOrders, тут я затрудняюсь дать пояснения, для чего я это сделал, но по всей видимости не работало =)
miniShop2.grid.<strong>customOrders</strong>

Далее:
getColumns: function () {
        var all = {
            id: {width: 35},
            //customer: {width: 100, renderer: function (val, cell, row) {
            //    return miniShop2.utils.userLink(val, row.data['user_id'], true);
            //}},
            customer: {width: 50},
            //num: {width: 20},
            //receiver: {width: 100},
            //createdon: {width: 20, renderer: miniShop2.utils.formatDate},
            //updatedon: {width: 20, renderer: miniShop2.utils.formatDate},
            cost: {width: 50, renderer: this._renderCost},
            cart_cost: {width: 50},
            delivery_cost: {width: 75},
            weight: {width: 50},
            status: {width: 50, renderer: miniShop2.utils.renderBadge},
            delivery: {width: 75},
            order_comment: {width: 100},
            payment: {width: 75},
            text_address: {width: 150},
            city: {width: 100}, //получатель
            region: {width: 75}, //дата доставки
            phone: {width: 100},  //телефон заказчика
            //context: {width: 50},
            actions: {width: 75, id: 'actions', renderer: miniShop2.utils.renderActions, sortable: false},
        };

Я добавил нужные мне поля.

text_address: {width: 150},
city: {width: 100}, //получатель
region: {width: 75}, //дата доставки
phone: {width: 100},  //телефон заказчика

Кстати, я не добавлял новых полей в таблицу, а использовал уже имеющийся, собственно поэтому у меня поле 'city' по сути является именем получателя, и т.д., не знаю, чем это аукнется в дальнейшем, но пока так как есть. Кстати, тут было бы тоже здорово, узнать ваше мнение, стоит ли завести новые поля или так тоже ничего?

Еще, что бы все заработало, нужно создать плагин.
Я назвал его orderGridChange
<?php
switch ($modx->event->name) {
    case 'msOnManagerCustomCssJs': 
         
        $modx->controller->addLastJavascript(MODX_ASSETS_URL.'custom/orderGridChange.js');
  
    break;
}
Плагину, нужно поставить галку в закладке системные события напротив msOnManagerCustomCssJs

И если, я ничего не упустил, то в завершение, в системных настройках ms2_order_grid_fields нужно указать перечень полей, нужных для вывода в таблице, у меня это выглядит так: id,num,customer,status,cost,delivery,payment,createdon,comment,order_comment,region,city,updatedon,text_address,phone

Всем удачи, и спасибо за помощь!
Дмитрий
16 марта 2025, 22:46
modx.pro
171
0

Комментарии: 6

Евгений Webinmd
17 марта 2025, 12:52
0
Вам нужно переопределить процессор getlist скорей всего. Посмотрите вот эту статейку, там есть кусок описания про переопредление. ВОзможно вам поможет.
modx.pro/howto/24899
    Дмитрий
    18 марта 2025, 13:41
    0
    Спасибо за наводку! Покурю… кстати, а где сами процессоры minishopa живут? Что называется, что бы за образец взять скрипты. Забегая вперед, я знаю, что изменять исходники — дурной тон=)
      Евгений Webinmd
      18 марта 2025, 13:53
      0
      Процессоры лежат тут:
      core/components/minishop2/processors/mgr/

      В статье описано как не изменять исходники, там же и про процессор написано.
      При большом желании можно изменить исходник, проверить работает ли сама идея и потом уже переопределять
        Дмитрий
        18 марта 2025, 15:16
        0
        да, да, именно так я и планировал поступить=) Спасибо!
          Дмитрий
          19 марта 2025, 23:09
          0
          Разобрался, все сделал! Спасибо за помощь!
            Дмитрий
            20 марта 2025, 00:16
            0
            Разобрался, все сделал! Спасибо за помощь!
        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
        6