ExportUsers - Экспорт данных в XLS/XLSX/CSV/JSON frontend/backend

Давно пытался найти нормальное решение для экспорта данных в excel, да так чтобы можно было оставить только необходимые поля, так как, какие нибудь, технические поля вроде type, contentType при выгрузке тех же страниц MODX не особо то и нужны. Кроме того каждое поле необходимо привести в читабельный вид. К таким полям относятся: published phptype: boolean. Чтобы при выводе в xls было написано не 0 или 1 а в место них подставилось значение Да или Нет. publishedon дата публикации которая хранится в unix формате приняла вид 01.01.2018
и много других полей которые требуют внимание к себе

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

Для решения этих задач был разработан этот компонент. Из него получился довольно не плохой конструктор запросов с последующим экспортом данных XLS/XLSX/CSV/JSON.

Демо frontend
http://demoexportusers.bustep.ru/catalog/

Демо backend
http://demoexportusers.bustep.ru/manager
Пользователь: manager
Пароль: manager


В общем то компонент помогает сократить время на внедрение таких решений как экспорт данных из MODX в форматы XLS/XLSX/CSV/JSON. По умолчанию в компонент уже добавлено несколько профилей для экспортирование данных: экспорт пользователей, экспорт заказов, экспорт ресурсов, экспорт категорий. Их можно брать за основу для создания своих профилей (профиль можно скопировать).

Что умеет приложение


  • Выгружать в форматы XLS/XLSX/CSV/JSON из административной части
  • Выгрузка данных с фронтенда (опцеональная так как требует кое какие операций для внедрения, ниже соледует описание)
  • Составлять список полей для выгрузки в удобно интерфейсе
  • Настройка и управление запросами для выгрузки
  • Настройка стилей для вывода в форматы xls и xlsx
  • Управление файлами через источники файлов MODX
  • Установка крон задания для выгрузки (к примеру если вам необходимо выгружать какие то данные периодические).
Сразу уточню несколько моментов, чтобы далее прочитанный текст не вводил ни кого в заблуждение:

  • Компонент не предназначен для экспорта данных во всяческие яндекс маркет и для экспорта 100к. тысячных таблиц с товарами. Для этого существует другой компонент msImportExport и тому подобные.
  • Время на ограничения по выгрузке полностью зависит от вашего времени работы php — max_execution_time и памяти memory_limit.Если у вас их не хватает max_execution_time или memory_limitто используйте limit и start для пропуск записей (во вкладке дополнительные запросы).
  • Компонент НЕ умеет ИМПОРТИРОВАТЬ данные, он необходим только для экспорта.
  • Выгружаемый вид данных настраиваются сугубо индивидуально. Как и запросы которые вы составляете.
  • Если вы выгружаете товары с классом msProduct то это не означает что данные из класса msProductData тоже смогут выгрузится автоматически. Для этого необходимо добавить свой join во вкладке дополнительных запросов.
    Пример: leftJoin:
    {"Data": {"class": "msProductData"}}
В общем то компонент представляет из себя наборы инструментов для облегчения выгрузки данных а не как полностью готовое решение по экспорту всего чего угодно и куда угодно.

Список профилей


Профиль можно скопировать и на основе него собрать уже свой. Один профиль можно выгружать в любом из форматов CSV,XLS,XLSX,JSON.



Экспорт данных


Процесс экспорта данных после нажатия на кнопки xls,xlsx,csv,json



Настройки


Основные настройки



Настройки файла



Можно задать своё имена файла, которое обработается функцией strftime

Дополнительные настройки



Поля для выгрузки


Импорт поле по классу с указанием префикса для импортируемого поля и назначение обработчика по умолчанию.



Таблица с полями

Составление списка полей в удобном интерфейс где можно задать значения для поле:
  • Название поля из базы
  • Название колонки для поля
  • Ширину колонки
  • Обработчик для значения


Массовое выделение полей (ctrl или shift) и удаления их. Так как при импорте как правило много лишних полей подгружается.
Редактирование полей в таблице с изменение названия, обработчика, ширины а так же позиции поля с помощью перетаскивания.

Дополнительные запросы


Отдельно добавлена возможность для указания своего процессора для обработчик данных. То есть вы можете расшить свой процессор методами из
core/components/exportusers/processors/mgr/export/default
и указать его для получения данных.



Для профиля в дополнительных запросах можно задать значения в json формате с подсветкой синтаксиса кода: LeftJoin, InnerJoin, where, select и groupBy, having, limit, start, sort, dir.



Кнопки Посмотреть SQL запрос тестирует на валидность написанный запрос. После нажатия отправляется запрос в базу, если sql содержит ошибку, к пример добавили не существующие поле то вернет ошибка с SQL кодом:



Кнопка Посмотреть массив с результатами вернет: общее количество записей, строку с заголовками и первую запись.



Excel/CSV


На этой вкладки расположились настройки которые относятся к выгрузке xsl,xls,cvs форматов
Для xls и xlsx
  • Название вкладки
  • Добавить заголовки — чекбокс для отключения заголовка из первой строки
  • Цвет строки с заголовком — если заголовок включен
  • Высота строки  высота всех строк
  • Ширина колонки  ширина колонки по умолчанию (можно индивидуально задавать ширину колонки для каждой колонки в списке полей)
  • Стиль колонок  для продвинутой настройки полей через механизмы библиотеки PHPExcel
CSV
  • Разделитель для CSV по умолчанию ;

JSON


Чекбокс Форматировать JSON который включает читабельный вид JSON

До включения:
[{"num":"\u041d\u043e\u043c\u0435\u0440 \u0437\u0430\u043a\u0430\u0437\u0430","createdon":"\u0421\u043e\u0437\u0434\u0430\u043d","cost":"\u041d\u0430 \u0441\u0443\u043c\u043c\u0443","cost_cart":"\u041e\u0431\u0449\u0430\u044f \u0441\u0443\u043c\u043c\u0430 \u0437\u0430\u043a\u0430\u0437\u0430","Payment.name":"\u0421\u043f\u043e\u0441\u043e\u0431 \u043e\u043f\u043b\u0430\u0442\u044b","Delivery.name":"\u0421\u043f\u043e\u0441\u043e\u0431 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438","Status.name":"\u0421\u0442\u0430\u0442\u0443\u0441","User.username":"\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c","Profile.fullname":"\u0424\u0418\u041e","Profile.phone":"\u0422\u0435\u043b\u0435\u0444\u043e\u043d"},{"num":"1807\/1","createdon":"2018-07-25 21:15:16","cost":"23500.00","cost_cart":"","Payment.name":"\u041e\u043f\u043b\u0430\u0442\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u044b\u043c\u0438","Delivery.name":"\u0421\u0430\u043c\u043e\u0432\u044b\u0432\u043e\u0437","Status.name":"\u041d\u043e\u0432\u044b\u0439","User.username":"inf@mail.ru","Profile.fullname":"\u0418\u0432\u0430\u043d\u043e\u0432 \u0418\u0432\u0430\u043d \u0418\u0432\u0430\u043d\u043e\u0432\u0438\u0447","Profile.phone":""},{"num":"1807\/2","createdon":"2018-07-25 21:15:48","cost":"38500.00","cost_cart":"","Payment.name":"\u041e\u043f\u043b\u0430\u0442\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u044b\u043c\u0438","Delivery.name":"\u0421\u0430\u043c\u043e\u0432\u044b\u0432\u043e\u0437","Status.name":"\u041d\u043e\u0432\u044b\u0439","User.username":"info@mail.ru","Profile.fullname":"\u0418\u0432\u0430\u043d\u043e\u0432 \u0418\u0432\u0430\u043d \u0418\u0432\u0430\u043d\u043e\u0432\u0438\u0447","Profile.phone":""}]
После:
[
    {
        "num": "Номер заказа",
        "createdon": "Создан",
        "cost": "На сумму",
        "cost_cart": "Общая сумма заказа",
        "Payment.name": "Способ оплаты",
        "Delivery.name": "Способ доставки",
        "Status.name": "Статус",
        "User.username": "Пользователь",
        "Profile.fullname": "ФИО",
        "Profile.phone": "Телефон"
    },
    {
        "num": "1807\/1",
        "createdon": "2018-07-25 21:15:16",
        "cost": "23500.00",
        "cost_cart": "",
        "Payment.name": "Оплата наличными",
        "Delivery.name": "Самовывоз",
        "Status.name": "Новый",
        "User.username": "inf@mail.ru",
        "Profile.fullname": "Иванов Иван Иванович",
        "Profile.phone": ""
    },
]

Экспорт данных с фронтенда


Наверное один из самых интересующих вопросов для этого компонента, как экспортировать данные с фронтенда?

С компонентом поставляется 2 сниппета:
ExportUsersExpanding — для вывода ссылок с разрешенными форматами для скачивания. Можно задать tpl и список форматов для вывода expanding а так же обязательны параметр profile имя профиля для экспорта.
[[!ExportUsersExpanding? 
    &profile=`Экспорт товаров` 
    &expanding=`xls,xlsx,json,csv` 
    &tpl=`@INLINE <a class="exportusers_btn" data-classexport="[[+classExport]]" data-profile="[[+profile]]" href="#">Скачать [[+classExport]]</a>` ]]
ExportUsers — предназначен для экспорта данных и скачивания.
[[!ExportUsers? &profile=`Экспорт товаров` ]]
Если с первым сниппетом все понятно, то со вторым возникает вопрос как она работает?

Начну с того что его нельзя просто так разместить на страницу и он сразу заработает.
Для каждого решения придется искать подход как передать данные в сниппет.

Для того чтобы сниппет экспортировал данные необходимо передать ему параметры:
  • data: массив с данным для экспорта (принимает json либо array)
  • profile: id экспортируемого профиля
  • classExport: формат для экспорта (в get запросе: xls,xlsx,json,csv)
Сниппет ExportUsers сам НЕ УМЕЕТ ДЕЛАТЬ ЗАПРОСЫ ПО УКАЗАННОМУ ПРОФИЛЮ

Потому что при экспорте с фронтенда как правило надо учитывать разные сортировки, фильтры и тому подобные параметры чтобы результаты экспорта совпадали с результатами отображаемыми на странице.

Для этого необходимо передавать полностью готовый массив с данными.

Пример
У нас есть сниппет msProducts который используется в mFilter2.Чтобы данные могли экспортировать с учетом всех фильтров наложенных mFilter2 необходимо добавить код в сниппет msProducts:

........................
// Merge all properties and run!
$pdoFetch->setConfig(array_merge($default, $scriptProperties), false);
$rows = $pdoFetch->run();

$modx->runSnippet('ExportUsers', array_merge($scriptProperties,array('data' => $rows)));

// Process rows
$output = array();
if (!empty($rows) && is_array($rows)) {
............................
Иначе невозможно будет экспортировать ваши данные в правильном порядке.

Естественно учет таких параметров как количество выгружаемых записей ложится точно также на сниппет msProduct. Если у вас стоит "показывать только по 10 записей (limit)" а по результатам общего количества у вас их 121, вам необходимо выставлять limit 121 у себя в сниппете msProducts а не 10 как это установлено по умолчанию для отображения на странице.

Если в адресной строке присутствует параметр get параметр classExport то в msProducts надо передавать limit:121

Грубо говоря сниппет умеет работать только с готовыми данными.

Как определить какие поля должен отдавать msProducts?
Если вы создали профиль с результатами данных для экспорта товаров то сниппет msProducts должен будет передавать набор полей подходящих под профиль.



Массив передаваемых полей должен соответствовать!!!



Обработчики полей


В компонент можно добавлять свои обработчики для полей. Чтобы создать свой обработчик необходим добавить его в папку с компонентом:
core/components/exportusers/custom/handlerfileds
Примеры рабочих обработчиков вы найдете в этой же папке.

По умолчанию в компонент добавлены обработчик
  • boolean — вернет Да или нет
  • date — преобразует дату в заданный формат из: Настройки → Дополнительные настройки → Формат даты (по умолчанию d.m.Y H:i:s)
  • gender — обработчик для класса modUserProfile с полем gender
  • groupsusers — Вернет наименование групп пользователей (в профилях поставляемых с компонентом можно посмотреть как он действует)
  • price — вернет цену обрабатываемую компонентом minishop функция formatPrice
  • url — добавит в начало значения site_url сайта
После добавления он автоматически появляется в список обработчиков для поля.

Обработчики для выгрузки


Так же вы можете добавить или кастомизировать свой обработчик для выгрузки данных в папке:
core/components/exportusers/controllers/export
Наименование файла будет являться форматом для выгрузки данных и автоматически появится в списке экспортируемых форматов.

Источники файлов


С компонентом поставляется свой источник файлов «ExportUsers», экспортирующий по умолчанию все данные в папку:
core/components/exportusers/export
Можно скопировать уже имеющийся источник файлов и назначить свой, или же изменить путь для сохранения файлов.

Дополнительно во вкладке Настройки → Настройки файлов→ Папка для экспортаможно задать относительны путь для выгрузки файлов.

К примеру если у вас в источнике файлов указан путь
core/components/exportusers/export то добавив users получится что ваши файлы будут выгружаться в папкуcore/components/exportusers/export/users


Скачивание файлов


По умолчанию доступ к файлам core/components/exportusers/export ограничен, то есть к ним нету прямого доступа.
По этому для скачивания был добавлен собственный контроллер находящийся в папке:
assets/components/exportusers/download.php
Через него производится скачивание всех файлов.

Чтобы скачать файл необходимо пройти по ссылке:
http://exportuser.ru/assets/components/exportusers/download.php?src=users%2Fexport_users+04.08.2018.xls&source=10&profile=244
Параметры url

  • srt — относительны путь к файл
  • source — id источника файлов
  • profile — id профиля
Вы можете открыть доступ для скачивания файлов чтобы не использовать контроллер.

Для этого необходимо указать свой путь в источнике файлов: к примеру:
basePath:assets/export/

Но в таком случае: галочка удалять файл после скачивания действовать не будет.

Дополнительно во вкладке Настройки → Настройка файловесть чекбоксы:

  • Автоматически скачивать после экспорта — функция больше нужна для админки, так как когда нажимаешь экспортировать и если стоит галочка то файл автоматически скачивает. Если галочку убрать то вернется ссылку для скачивания этого файла.
  • Удалять файл после скачивания — после того как мы экспортировали файл он хранится в назначенной папке. И мы хотим его удалить после того как начнется загрузка файла. Для этого устанавливаем галочку.
В случае если файл был удален или параметры были заданы не верно то пользователю автоматические отдастся страница 404 (параметр error_page) заданная в настройках по умолчанию или можно задать свою страницу в настройках компонента, есть параметр: exportusers_error_page

Политика доступа к файлу


Права доступа на скачивания устанавливаются через Источники файлов → ExportUsers → Права доступа

С компонент добавляется политика доступа для медиа ресурсов ExportUsersMedia которая регулирует доступ для скачивания.



После этой операции скачивать файл смогут только назначенные группы пользователей.

По умолчанию скачивать файлы могут все.

В контроллере происходит проверка прав доступа: file_view для источника файлов а не для контекста

Пришлось немного разобраться с правами доступа так как почему то проверка прав на источник файлов через функцию $source->hasPermission происходила для объекта modAccessibleObject а не для sources.modAccessMediaSource

В общем чтобы права проверялись правильно, я сделалал так:
if (!$source->checkPolicy('file_view', 'sources.modAccessMediaSource')) {
    $modx->sendUnauthorizedPage();
}
Теперь если у пользователя нету прав доступа ему будет показываться страница авторизации.
Адрес контроллера для скачивания так же меняется в источнике файлов в параметре >baseUrl

Для экспорта данных на php


Классы для экспорта устроены так чтобы их можно было по разному использовать у себя на сайте.

Экспорт данных из профиля
<?php
/* @var ExportUsers $ExportUsers */
/* @var ExportUsersProfileHandler $Profile */
/** @var ExportUsers $ExportUsers */
$ExportUsers = $modx->getService('ExportUsers', 'ExportUsers', MODX_CORE_PATH . 'components/exportusers/model/');
if (!$ExportUsers->initialize(true)) {
    return 'error load class ExportUsers';
}
$profile = 'Экспорт товаров';
if ($ExportUsersProfile = $modx->getObject('ExportUsersProfile', array('name' => $profile))) {
   
    // По умолчанию данные будут выгружаться из основного процессора определенного для профиля
    // Для записи своих данных используйте  $ExportUsersProfile->setData($data);
    
    /* @var ExportUsersProfileHandler $Profile */
    if ($Profile = $ExportUsers->newExportProfile($ExportUsersProfile, $scriptProperties)) {
        if ($export = $Profile->export($classExport)) {
            if ($export->save()) {
                if ($file = $export->loadFile()) {
                    $file->download();
                }
            }
       }
    }
}

Экспорт данных с просмотром конфига

Преимущества в том что можно предварительно посмотреть что экспортируется перед начало экспорта.
<?php
/* @var ExportUsers $ExportUsers */
/* @var ExportUsersProfileHandler $Profile */
/** @var ExportUsers $ExportUsers */
$ExportUsers = $modx->getService('ExportUsers', 'ExportUsers', MODX_CORE_PATH . 'components/exportusers/model/');
if (!$ExportUsers->initialize(true)) {
    return 'error load class ExportUsers';
}
$profile = 'Экспорт товаров';
if ($ExportUsersProfile = $modx->getObject('ExportUsersProfile', array('name' => $profile))) {
    if ($Profile = $ExportUsers->newExportProfile($ExportUsersProfile)) {
        
        echo '<pre>'; 
        print_r($Profile->toArray()); die;
        
        if ($export = $Profile->export($classExport)) {
            if ($export->save()) {
                if ($file = $export->loadFile()) {
                    echo '<pre>';
                    print_r($file->downloadLink());
                    die;
                }
            }
       }
    }
}

Экcпорт данных с модификацией профиля

<?php
/* @var ExportUsers $ExportUsers */
/* @var ExportUsersProfileHandler $Profile */
/** @var ExportUsers $ExportUsers */
$ExportUsers = $modx->getService('ExportUsers', 'ExportUsers', MODX_CORE_PATH . 'components/exportusers/model/');
if (!$ExportUsers->initialize(true)) {
    return 'error load class ExportUsers';
}
$profile = 'Экспорт товаров';
if ($ExportUsersProfile = $modx->getObject('ExportUsersProfile', array('name' => $profile))) {
  
    $config = $ExportUsersProfile->getConfig();
    $handlers = $ExportUsersProfile->getHandlers();
    $widths = $ExportUsersProfile->getWidths();
    $fields = $ExportUsersProfile->getFields();
    $data = $ExportUsersProfile->getData();
    
    
    $widths = array_merge($widths, array(
        'longtitle' => 150
    ));
    
    $config = array_merge($config, array(
        'filename' => 'export product %d.%m.%Y',
        'head_process' => false,
        'path' => 'array/',
        'remove' => 0,
    ));
    
    if ($Profile = $ExportUsers->profile->newHandler($data, $config, $fields, $widths, $handlers)) {
        if ($export = $Profile->export($classExport)) {
            if ($export->save()) {
                if ($file = $export->loadFile()) {
                    $file->download();
                }
            }
        }
    };
}


Экcпорт данных без профиля

Вы можете экспортировать данные без профиля но при это вся обработка значений полей ложится на вас.

<?php
/* @var ExportUsers $ExportUsers */
/* @var ExportUsersProfileHandler $Profile */
/** @var ExportUsers $ExportUsers */
$ExportUsers = $modx->getService('ExportUsers', 'ExportUsers', MODX_CORE_PATH . 'components/exportusers/model/');
if (!$ExportUsers->initialize(true)) {
    return 'error load class ExportUsers';
}
$classExport = 'xlsx';
$config = array(
    'filename' => 'export product %d.%m.%Y',
    'head_process' => false,
    'path' => 'array/',
    'remove' => 0,
);

// Записываем контент для экспорта
$data = array(
    array(
           "id" => "102",
          "pagetitle"=>  "Погружной блендер Philips HR1377",
          "longtitle"=>  "Погружной блендер Philips HR1377",
          "description"=>  "",
          "old_price"=>  0,
          "content"=>  "\"<p>Основные характеристики:</p><p>Тип погружной</p><p>Мощность 700 Вт</p><p>Управление механическое, число скоростей: 5, плавная регулировка скорости</p><p>Особенности:</p><p>Материал корпуса металл</p><p>Материал погружной части металл</p><p>Отверстие для ингредиентов есть</p><p>Сетевой шнур длина 1.30 м</p><p>Функциональные возможности:</p><p>Дополнительные режимы турборежим</p><p>Измельчитель есть, объем 1.5 л</p><p>Мерный стакан есть, объем 1 л</p><p>Венчик для взбивания есть</p><p>Дополнительная информация диск для нарезки тонкими/средними ломтиками",
          "published"=>  "Да",
          "Data.price"=>  "21 130",
          "uri"=>  "http://exportuser.ru/catalog/product-102.html",
          "introtext"=>  "Погружной блендер Philips HR1377",
          "parent"=>  "2"
      )
);

$fields = array(
    'id' => 'ID',
    'pagetitle' => 'Заголовок',
    'longtitle' => 'Заголовок',
    'content' => 'Контент',
    'description' => 'Описание',
    'old_price' => 0,
    'published' => 1,
    'Data.price' => "Цена",
    'uri' => "Ссылка на товар",
    'introtext' => "Вступительный текст",
);
$widths = array_merge($widths, array(
    'longtitle' => 150
));
$handlers = array_merge($widths, array(
    'published' => 'boolean'
));
if ($Profile = $ExportUsers->profile->newHandler($data, $config, $fields, $widths, $handlers)) {
    if ($export = $Profile->export($classExport)) {
        if ($export->save()) {
            if ($file = $export->loadFile()) {
                $file->download();
            }
        }
    }
};

Ссылка на приложение ExportUsers . цена: 1990 руб.

Компонент получился достаточно крупный и с большим количеством возможностей.

Готовые профили для экспорта данных
во время установки добавляются готовые профили.

  • Экспорт заказов minishop (файлы XLS, XLSX, CSV)
  • Экспорт ресурсов (файлы XLS, XLSX, CSV)
  • Экспорт пользователей (файлы XLS, XLSX, CSV)
Андрей Степаненко
06 августа 2018, 14:01
modx.pro
5
6 287
+6
Поблагодарить автора Отправить деньги

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

Здоров Александр
08 августа 2018, 22:18
1
+1
Спасибо за ваше решение!
В подобных статьях начинаешь думать, что вариант создания навигационного меню справа, с закреплением на экране было бы удобно, или хотя бы в начале статьи
start.exe
12 августа 2018, 11:48
0
Есть ли возможность в файле со списком заказов выводить и список товаров в каждом заказе?
Игорь
15 августа 2018, 08:56
0
Андрей, если бы еще у этого же дополнения был и импорт — цены бы ему не было.
    Андрей Степаненко
    15 августа 2018, 09:33
    0
    При учете что начинающие программисты хотят испытать все возможность компонентов, будут совершатся множественные ошибки во время импорта, это потребует технической поддержки так как надо будет обеспечить поддержку возможностей Экспорта и обеспечить возможности Импорта.

    Причем по опыту могу сказать что импорт таблички товаров в csv формате занимает десятки часов для приведения в нормальный вид на фронтенде.

    Нормальный вид — это когда через неделю или месяц у заказчика не возникает вопроса: а может быть еще вот так сделаем? или нужно еще немного доработать.

    Тогда компонент будет стоить 9999 руб. Ибо обеспечение техподдержки для этих обеих процедур экспорт и импорт требует большего внимание, чем только к экспорту или только к импорту.
    Alexandr
    25 июня 2019, 22:08
    0
    Здравствуйте. Установил ваш компонент. при экспорте заказов (orders) выдает:

    [ExportUsersPHPExcelDefaultController][441] validateFields: Error content empty
    Произошла ошибка во время экспорт. Подробная информация в логах

    error.log при этом чистый.

    Насколько я понимаю, проблема в функции validateFields(). $content — пустая. Куда следует копать дальше?
    Настройки orders не менял. Все настройки ваши «из коробки»
      Андрей Степаненко
      26 июня 2019, 04:06
      0
      Возможно проблема в источниках фалов, у вас директория core перенесена?
        Alexandr
        26 июня 2019, 12:30
        0
        Нет, все стандартно
        Sergikovich
        10 декабря 2020, 19:05
        0
        Возникла такая же проблема, получилось решить?
          Александр
          12 сентября 2022, 23:02
          0
          Тоже столкнулся с проблемой. Есть решение у кого?
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          17