diff --git a/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php b/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php new file mode 100755 index 0000000..dc83d66 --- /dev/null +++ b/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php @@ -0,0 +1,345 @@ + 'Светлая (light)', + 'dark' => 'Тёмная (dark)', + 'cupcake' => 'Капкейк (cupcake)', + 'bumblebee' => 'Шмель (bumblebee)', + 'emerald' => 'Изумруд (emerald)', + 'corporate' => 'Корпоративная (corporate)', + 'synthwave' => 'Синтвейв (synthwave)', + 'retro' => 'Ретро (retro)', + 'cyberpunk' => 'Киберпанк (cyberpunk)', + 'valentine' => 'Валентинка (valentine)', + 'halloween' => 'Хэллоуин (halloween)', + 'garden' => 'Сад (garden)', + 'forest' => 'Лес (forest)', + 'aqua' => 'Аква (aqua)', + 'lofi' => 'Лоу-фай (lofi)', + 'pastel' => 'Пастель (pastel)', + 'fantasy' => 'Фэнтези (fantasy)', + 'wireframe' => 'Каркас (wireframe)', + 'black' => 'Чёрная (black)', + 'luxury' => 'Люкс (luxury)', + 'dracula' => 'Дракула (dracula)', + 'cmyk' => 'CMYK (cmyk)', + 'autumn' => 'Осень (autumn)', + 'business' => 'Бизнес (business)', + 'acid' => 'Кислотная (acid)', + 'lemonade' => 'Лимонад (lemonade)', + 'night' => 'Ночная (night)', + 'coffee' => 'Кофейная (coffee)', + 'winter' => 'Зимняя (winter)', + 'dim' => 'Тусклая (dim)', + 'nord' => 'Нордическая (nord)', + 'sunset' => 'Закат (sunset)', + 'caramellatte' => 'Карамель-латте (caramellatte)', + 'abyss' => 'Бездна (abyss)', + 'silk' => 'Шёлк (silk)', + ]; + + private $error = array(); + + public function index() + { + $this->load->language('extension/module/tgshop'); + $this->load->model('setting/setting'); + + $hasConfig = $this->config->get('module_tgshop_app_name') !== null; + + if ($hasConfig) { + $this->config(); + } else { + $this->init(); + } + } + + private function config(): void + { + $data = []; + $this->document->setTitle($this->language->get('heading_title')); + + if (($this->request->server['REQUEST_METHOD'] === 'POST') && $this->validate()) { + $this->model_setting_setting->editSetting('module_tgshop', $this->request->post); + + $this->session->data['success'] = $this->language->get('text_success'); + + $this->response->redirect( + $this->url->link( + 'extension/module/tgshop', + 'user_token=' . $this->session->data['user_token'] . '&type=module', + true + ) + ); + } + + $this->baseData($data); + + $data['action'] = $this->url->link( + 'extension/module/tgshop', + 'user_token=' . $this->session->data['user_token'], + true + ); + + $data['settings'] = static::$settings; + + foreach (static::$settings as $configs) { + foreach ($configs as $key => $config) { + if ($config['type'] === 'image') { + $this->load->model('tool/image'); + if (isset($this->request->post[$key]) && is_file(DIR_IMAGE . $this->request->post[$key])) { + $data[$key] = $this->model_tool_image->resize($this->request->post[$key], 100, 100); + } elseif ($this->config->get($key) && is_file(DIR_IMAGE . $this->config->get($key))) { + $data[$key] = $this->model_tool_image->resize($this->config->get($key), 100, 100); + } else { + $data[$key] = $this->model_tool_image->resize('no_image.png', 100, 100); + } + } elseif ($config['type'] === 'products') { + $products = $this->request->post[$key] ?? $this->config->get($key) ?? []; + $this->load->model('catalog/product'); + foreach ($products as $productId) { + $productItem = $this->model_catalog_product->getProduct($productId); + $data[$key][] = [ + 'product_id' => $productId, + 'name' => $productItem['name'], + ]; + } + } else { + if (isset($this->request->post[$key])) { + $data[$key] = $this->request->post[$key]; + } else { + $data[$key] = $this->config->get($key); + } + } + } + } + + $this->response->setOutput($this->load->view('extension/module/tgshop', $data)); + } + + public function init(): void + { + $data = []; + $this->baseData($data); + + $data['action'] = $this->url->link( + 'extension/module/tgshop/init', + 'user_token=' . $this->session->data['user_token'], + true + ); + + if ($this->request->server['REQUEST_METHOD'] === 'POST') { + $defaults = $this->getDefaultConfig(); + $this->model_setting_setting->editSetting('module_tgshop', $defaults); + $this->session->data['success'] = 'Инициализация модуля выполнена успешно.'; + $this->response->redirect( + $this->url->link( + 'extension/module/tgshop', + 'user_token=' . $this->session->data['user_token'], + true + ) + ); + } + + $this->response->setOutput($this->load->view('extension/module/tgshop_init', $data)); + } + + protected function validate() + { + if (! $this->user->hasPermission('modify', 'extension/module/tgshop')) { + $this->error['error_warning'] = $this->language->get('error_permission'); + } + + foreach (static::$settings as $configs) { + foreach ($configs as $key => $config) { + if (($config['required'] ?? false) === true && ! $this->request->post[$key]) { + $this->error["error_$key"] = 'Поле "' . $this->language->get( + "lbl_$key" + ) . '" обязательно для заполнения.'; + } + } + } + + return ! $this->error; + } + + private function baseData(array &$data): void + { + $this->document->setTitle($this->language->get('heading_title')); + + $data['header'] = $this->load->controller('common/header'); + $data['column_left'] = $this->load->controller('common/column_left'); + $data['footer'] = $this->load->controller('common/footer'); + + $data['cancel'] = $this->url->link( + 'marketplace/extension', + 'user_token=' . $this->session->data['user_token'] . '&type=module', + true + ); + + $data = array_merge($data, $this->error); + + $data['breadcrumbs'] = array(); + + $data['breadcrumbs'][] = array( + 'text' => $this->language->get('text_home'), + 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true) + ); + + $data['breadcrumbs'][] = array( + 'text' => $this->language->get('text_module'), + 'href' => $this->url->link( + 'marketplace/extension', + 'user_token=' . $this->session->data['user_token'] . '&type=module', + true + ) + ); + + $data['breadcrumbs'][] = array( + 'text' => $this->language->get('heading_title'), + 'href' => $this->url->link( + 'extension/module/tgshop', + 'user_token=' . $this->session->data['user_token'], + true + ) + ); + + if (isset($this->session->data['success'])) { + $data['success'] = $this->session->data['success']; + + unset($this->session->data['success']); + } else { + $data['success'] = ''; + } + + $data['user_token'] = $this->session->data['user_token']; + } + + private function getDefaultConfig(): array + { + return [ + 'module_tgshop_status' => 1, + 'module_tgshop_app_name' => $this->config->get('config_meta_title'), + 'module_tgshop_app_icon' => $this->config->get('config_image') ?: $this->model_tool_image->resize( + 'no_image.png', + 100, + 100 + ), + 'module_tgshop_owner_notification_template' => 'Новый заказ!', + 'module_tgshop_theme_light' => 'light', + 'module_tgshop_theme_dark' => 'dark', + 'module_tgshop_mainpage_products' => 'most_viewed', + 'module_tgshop_featured_products' => [], + ]; + } + + private function getSettingsConfig(): array + { + return [ + 'general' => [ + 'module_tgshop_status' => [ + 'type' => 'select', + 'options' => [ + 0 => 'Выключено', + 1 => 'Включено', + ], + 'help' => '', + ], + + 'module_tgshop_app_name' => [ + 'hidden' => true, + 'required' => true, + 'type' => 'text', + 'placeholder' => 'Введите название Телеграм магазина', + 'help' => << [ + 'hidden' => true, + 'type' => 'image', + 'help' => << [ + 'type' => 'select', + 'options' => static::$themes, + 'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для дневного режима. Посмотреть как выглядят темы', + ], + + 'module_tgshop_theme_dark' => [ + 'type' => 'select', + 'options' => static::$themes, + 'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для ночного режима. Посмотреть как выглядят темы', + ], + ], + 'telegram' => [ + 'module_tgshop_bot_token' => [ + 'type' => 'text', + 'placeholder' => 'Введите токен от телеграм бота', + 'help' => << [ + 'type' => 'chatid', + 'placeholder' => 'Введите Chat ID', + 'help' => << [ + 'type' => 'tg_message_template', + 'placeholder' => 'Введите текст уведомления', + 'rows' => 15, + 'help' => 'Введите шаблон сообщения для Telegram-уведомлений о новом заказе владельцу магазина.', + ], + 'module_tgshop_customer_notification_template' => [ + 'type' => 'tg_message_template', + 'placeholder' => 'Введите текст уведомления', + 'rows' => 15, + 'help' => 'Введите шаблон сообщения для Telegram-уведомлений о новом заказе покупателю.', + ], + ], + 'statistics' => [ + 'module_tgshop_yandex_metrika' => [ + 'type' => 'textarea', + 'placeholder' => 'Вставьте код счётчика Яндекс Метрики', + 'rows' => 15, + 'help' => '' + ], + ], + + 'shop' => [ + 'module_tgshop_mainpage_products' => [ + 'type' => 'select', + 'options' => [ + 'most_viewed' => 'Популярные товары', + 'featured' => 'Избранные товары (задать в поле ниже)', + ], + 'help' => 'Выберите, какие товары показывать на главной странице магазина в Telegram. Это влияет на первую видимую секцию каталога для пользователя.', + ], + + 'module_tgshop_featured_products' => [ + 'type' => 'products', + 'help' => 'На главной странице будут отображаться избранные товары, если вы выберете этот вариант в настройке “Товары на главной”.', + ], + ], + + 'orders' => [ + + ], + ]; + } +} diff --git a/module/oc_telegram_shop/upload/admin/language/ru-ru/extension/module/tgshop.php b/module/oc_telegram_shop/upload/admin/language/ru-ru/extension/module/tgshop.php new file mode 100755 index 0000000..c672695 --- /dev/null +++ b/module/oc_telegram_shop/upload/admin/language/ru-ru/extension/module/tgshop.php @@ -0,0 +1,32 @@ + + +
+ {% if error_warning %} +
{{ error_warning }} + +
+ {% endif %} + {% if success %} +
{{ success }} + +
+ {% endif %} +
+
+

{{ text_edit }}

+
+
+
+ +
+                        * Проверка request от телеграм
+                        4. Выбор товаров, которые будут отображаться на главной странице
+                                                        1. Шаблон для уведомлений покупателя о заказе
+                                2. Требовать ввод email/phone
+                                3. Группа покупателей для заказов от ТГ
+                    
+ + + +
+ {% for tabKey, tabItems in settings %} +
+ {% for settingKey, item in tabItems %} +
+ +
+ {# Select #} + {% if item['type'] == 'select' %} + + + {# Text Input #} + {% elseif item['type'] == 'text' %} + + + {# Image #} + {% elseif item['type'] == 'image' %} + + + + + + {# Image #} + {% elseif item['type'] == 'textarea' %} + + + {# Products #} + {% elseif item['type'] == 'products' %} + +
+ {% for product in attribute(_context, settingKey) %} +
+ {{ product.name }} + +
+ {% endfor %} +
+ + + {# ChatID #} + {% elseif item['type'] == 'chatid' %} +
+ + + + + +
+ + +
+
+

Как получить Chat ID

+
    +
  1. Убедитесь, что вы ввели Telegram Bot Token выше.
  2. +
  3. Откройте вашего бота в Telegram и отправьте ему любое сообщение.
  4. +
  5. Вернитесь сюда и нажмите кнопку «Получить Chat ID» — мы автоматически подставим его в поле ниже.
  6. +
+
+
+ + {% elseif item['type'] == 'tg_message_template' %} +
+ +
+ +
+ + +
+ +
+
+

Вы можете использовать переменные:

+
    +
  • {store_name} — название магазина
  • +
  • {order_id} — номер заказа
  • +
  • {customer} — имя и фамилия покупателя
  • +
  • {email} — email покупателя
  • +
  • {phone} — телефон
  • +
  • {comment} — комментарий к заказу
  • +
  • {address} — адрес доставки
  • +
  • {total} — сумма заказа
  • +
  • {ip} — IP покупателя
  • +
  • {created_at} — дата и время создания заказа
  • +
+

Форматирование: поддерживается *MarkdownV2* .

+

Символы, которые нужно экранировать в тексте:

+
_ * [ ] ( ) ~ ` > # + - = | { } . !
+

Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования. Например вместо Заказ #123 нужно писать Заказ \#123.

+ +
+
+ + {% else %} + Unsupported {{ item|json_encode }} + {% endif %} + + {% if attribute(_context, 'error_' ~ settingKey) %} +
{{ attribute(_context, 'error_' ~ settingKey) }}
+ {% endif %} + + {% if item['help'] %} +

{{ item['help'] }}

+ {% endif %} +
+
+ + {% endfor %} +
+ {% endfor %} +
+
+
+
+
+ +{{ footer }} \ No newline at end of file diff --git a/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop_init.twig b/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop_init.twig new file mode 100755 index 0000000..3f4b9ba --- /dev/null +++ b/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop_init.twig @@ -0,0 +1,48 @@ +{{ header }}{{ column_left }} +
+ +
+ {% if error_warning %} +
{{ error_warning }} + +
+ {% endif %} +
+
+

Инициализация модуля

+
+
+ +
+

Инициализация модуля

+

Модуль запускается первый раз, поэтому необходимо инициализировать настройки для его корректной работы.

+
+ +
+
+ + +
+
+
+
+{{ footer }} \ No newline at end of file diff --git a/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php b/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php index b6e88d3..9059ce3 100755 --- a/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php +++ b/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php @@ -38,9 +38,18 @@ class Controllerextensiontgshophandle extends Controller 'language_id' => (int) $this->config->get('config_language_id'), 'shop_base_url' => HTTPS_SERVER, 'dir_image' => DIR_IMAGE, + 'app_name' => $this->config->get('module_tgshop_app_name'), + 'app_icon' => $this->config->get('module_tgshop_app_icon'), + 'theme_light' => $this->config->get('module_tgshop_theme_light'), + 'theme_dark' => $this->config->get('module_tgshop_theme_dark'), + 'mainpage_products' => $this->config->get('module_tgshop_mainpage_products'), + 'featured_products' => $this->config->get('module_tgshop_featured_products'), + 'ya_metrika_enabled' => !empty(trim($this->config->get('module_tgshop_yandex_metrika'))), 'telegram' => [ - 'bot_token' => '7513204587:AAGvRL15OzzltESqwbL15vOEWi6ZZMikDpg', - 'chat_id' => 849193407, + 'bot_token' => $this->config->get('module_tgshop_bot_token'), + 'chat_id' => $this->config->get('module_tgshop_chat_id'), + 'owner_notification_template' => $this->config->get('module_tgshop_owner_notification_template'), + 'customer_notification_template' => $this->config->get('module_tgshop_customer_notification_template'), ], 'db' => [ 'host' => DB_HOSTNAME, @@ -61,6 +70,7 @@ class Controllerextensiontgshophandle extends Controller $app->bind(Currency::class, function () { return $this->currency; }); + $app->bind(Tax::class, function () { return $this->tax; }); @@ -88,4 +98,33 @@ class Controllerextensiontgshophandle extends Controller $app->bootAndHandleRequest(); } + + function extractPureJs($input) { + // Убираем + $input = preg_replace('##is', '', $input); + + // Убираем + $input = preg_replace('##s', '', $input); + + // Извлекаем содержимое + if (preg_match('#]*>(.*?)#is', $input, $matches)) { + return trim($matches[1]); + } + + return ''; + } + + public function ya_metrika(): void + { + $raw = html_entity_decode($this->config->get('module_tgshop_yandex_metrika'), ENT_QUOTES | ENT_HTML5); + $raw = $this->extractPureJs($raw); + + http_response_code(200); + header('Content-Type: application/javascript'); + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST'); + header('Access-Control-Allow-Headers: Content-Type, Authorization'); + header('Access-Control-Allow-Credentials: true'); + echo $raw; + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env b/module/oc_telegram_shop/upload/oc_telegram_shop/.env old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Config/Settings.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Config/Settings.php index 3421f05..a961b4b 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Config/Settings.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Config/Settings.php @@ -42,4 +42,9 @@ class Settings { $this->settings = $settings; } + + public function getHash(): string + { + return md5(serialize($this->settings)); + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageTool.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageTool.php index ea413fa..bbb18bd 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageTool.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageTool.php @@ -19,7 +19,7 @@ class ImageTool implements ImageToolInterface $this->manager = new ImageManager(['driver' => $driver]); } - public function resize(string $path, int $width, int $height, ?string $default = null): ?string + public function resize(string $path, int $width, int $height, ?string $default = null, string $format = 'webp'): ?string { $filename = is_file($this->imageDir . $path) ? $path : $default; @@ -33,7 +33,7 @@ class ImageTool implements ImageToolInterface } $extless = utf8_substr($filename, 0, utf8_strrpos($filename, '.')); - $imageNew = 'cache/' . $extless . '-' . $width . 'x' . $height . '.webp'; + $imageNew = 'cache/' . $extless . '-' . $width . 'x' . $height . '.' . $format; $fullNewPath = $this->imageDir . $imageNew; $fullOldPath = $this->imageDir . $filename; @@ -49,7 +49,7 @@ class ImageTool implements ImageToolInterface $constraint->upsize(); }); - $image->encode('webp', 75)->save($fullNewPath, 75, 'webp'); + $image->encode($format, 75)->save($fullNewPath, 75, $format); } return rtrim($this->siteUrl, '/') . '/image/' . str_replace($this->imageDir, '', $fullNewPath); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageToolInterface.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageToolInterface.php index 1aa30fb..da86598 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageToolInterface.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageToolInterface.php @@ -4,5 +4,11 @@ namespace Openguru\OpenCartFramework\ImageTool; interface ImageToolInterface { - public function resize(string $path, int $width, int $height, ?string $default = null): ?string; + public function resize( + string $path, + int $width, + int $height, + ?string $default = null, + string $format = 'webp' + ): ?string; } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Router/Router.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Router/Router.php index c66a74b..3f95d7c 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Router/Router.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Router/Router.php @@ -25,7 +25,7 @@ class Router */ public function resolve($action): array { - if (!$action) { + if (! $action) { throw new ActionNotFoundException('No action provided'); } @@ -35,4 +35,14 @@ class Router throw new ActionNotFoundException('Action "' . $action . '" not found.'); } + + public function url(string $action, array $query = []): string + { + $query = array_merge([ + 'route' => 'extension/tgshop/handle', + 'api_action' => $action, + ], $query); + + return '/index.php?' . http_build_query($query); + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramService.php old mode 100644 new mode 100755 index 73c752d..03af796 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramService.php @@ -10,31 +10,47 @@ class TelegramService { private Logger $logger; private Client $client; + private string $botToken; public function __construct(string $botToken, Logger $logger) { $this->logger = $logger; + $this->botToken = $botToken; $this->client = $this->createGuzzleClient("https://api.telegram.org/bot{$botToken}/"); } + public function escapeTelegramMarkdownV2(string $text): string { + $specials = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']; + foreach ($specials as $char) { + $text = str_replace($char, '\\' . $char, $text); + } + return $text; + } + + public function prepareMessage(string $template, array $variables = []): string + { + $values = array_map([$this, 'escapeTelegramMarkdownV2'], array_values($variables)); + + return str_replace(array_keys($variables), $values, $template); + } + public function sendMessage(int $chatId, string $text): bool { - try { - $query = [ - 'chat_id' => $chatId, - 'text' => $text, - 'parse_mode' => 'MarkdownV2', - ]; - - $this->client->get('sendMessage', [ - 'query' => $query, - ]); - return true; - } catch (Exception $exception) { - $this->logger->error('Telegram sendMessage error: ' . json_encode($query)); - $this->logger->logException($exception); + if (! $this->botToken) { return false; } + + $query = [ + 'chat_id' => $chatId, + 'text' => $text, + 'parse_mode' => 'MarkdownV2', + ]; + + $this->client->get('sendMessage', [ + 'query' => $query, + ]); + + return true; } private function createGuzzleClient(string $uri): Client @@ -44,4 +60,11 @@ class TelegramService 'timeout' => 5.0, ]); } + + public function setBotToken(string $botToken): TelegramService + { + $this->botToken = $botToken; + + return $this; + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramServiceProvider.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramServiceProvider.php old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Decorators/OcRegistryDecorator.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Decorators/OcRegistryDecorator.php old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php index 1a9995c..4822997 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php @@ -46,8 +46,13 @@ class ProductsHandler $page = $request->get('page', 1); $perPage = 6; $categoryId = (int) $request->get('categoryId', 0); + $categoryName = ''; + $forMainPage = $categoryId === 0; + $featuredProducts = $this->settings->get('featured_products'); + $mainpageProducts = $this->settings->get('mainpage_products'); + $imageWidth = 200; $imageHeight = 200; @@ -85,7 +90,13 @@ class ProductsHandler ->where('product_to_category.category_id', '=', $categoryId); } ); - }); + }) + ->when( + $forMainPage && $mainpageProducts === 'featured' && $featuredProducts, + function (Builder $query) use ($featuredProducts) { + $query->whereIn('products.product_id', $featuredProducts); + } + ); $total = $productsQuery->count(); $lastPage = PaginationHelper::calculateLastPage($total, $perPage); @@ -93,7 +104,7 @@ class ProductsHandler $products = $productsQuery ->forPage($page, $perPage) - ->orderBy('date_added', 'DESC') + ->orderBy('viewed', 'DESC') ->get(); $productIds = Arr::pluck($products, 'product_id'); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php new file mode 100755 index 0000000..116352d --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php @@ -0,0 +1,120 @@ +settings = $settings; + $this->imageTool = $imageTool; + $this->router = $router; + $this->telegramService = $telegramService; + } + + public function index(): JsonResponse + { + $appIcon = $this->settings->get('app_icon'); + $icon192 = $this->imageTool->resize($appIcon, 192, 192, 'no_image.png', 'png'); + $icon180 = $this->imageTool->resize($appIcon, 180, 180, 'no_image.png', 'png'); + $icon152 = $this->imageTool->resize($appIcon, 152, 152, 'no_image.png', 'png'); + $icon120 = $this->imageTool->resize($appIcon, 120, 120, 'no_image.png', 'png'); + $hash = $this->settings->getHash(); + + return new JsonResponse([ + 'app_name' => $this->settings->get('app_name'), + 'app_icon' => $appIcon . '?_v=' . $hash, + 'app_icon192' => $icon192 . '?_v=' . $hash, + 'app_icon180' => $icon180 . '?_v=' . $hash, + 'app_icon152' => $icon152 . '?_v=' . $hash, + 'app_icon120' => $icon120 . '?_v=' . $hash, + 'manifest_url' => $this->router->url('manifest', ['_v' => $hash]), + 'theme_light' => $this->settings->get('theme_light'), + 'theme_dark' => $this->settings->get('theme_dark'), + 'ya_metrika_enabled' => $this->settings->get('ya_metrika_enabled'), + ]); + } + + public function manifest(): JsonResponse + { + $appIcon = $this->settings->get('app_icon'); + $icon192 = $this->imageTool->resize($appIcon, 192, 192, 'no_image.png', 'png'); + $icon512 = $this->imageTool->resize($appIcon, 512, 512, 'no_image.png', 'png'); + + return new JsonResponse([ + 'name' => $this->settings->get('app_name'), + 'short_name' => $this->settings->get('app_name'), + 'start_url' => '/image/catalog/tgshopspa/', + 'display' => 'standalone', + 'background_color' => '#ffffff', + 'theme_color' => '#000000', + 'orientation' => 'portrait', + 'icons' => [ + [ + 'src' => $icon192, + 'sizes' => '192x192', + 'type' => 'image/png', + ], + [ + 'src' => $icon512, + 'sizes' => '512x512', + 'type' => 'image/png', + ] + ] + ]); + } + + public function testTgMessage(Request $request): JsonResponse + { + $template = $request->json('template', 'Нет шаблона'); + $token = $request->json('token'); + $chatId = $request->json('chat_id'); + + $variables = [ + '{store_name}' => $this->settings->get('oc_store_name'), + '{order_id}' => 777, + '{customer}' => 'Иван Вастльевич', + '{email}' => 'telegram@opencart.com', + '{phone}' => '+79999999999', + '{comment}' => 'Это тестовый заказ', + '{address}' => 'г. Москва', + '{total}' => 100000, + '{ip}' => '127.0.0.1', + '{created_at}' => date('Y-m-d H:i:s'), + ]; + + $message = $this->telegramService->prepareMessage($template, $variables); + + try { + $this->telegramService + ->setBotToken($token) + ->sendMessage($chatId, $message); + return new JsonResponse([ + 'message' => 'Сообщение отправлено. Проверьте Telegram.', + ]); + } catch (ClientException $exception) { + $json = json_decode($exception->getResponse()->getBody(), true); + return new JsonResponse([ + 'message' => $json['description'], + ]); + } catch (Exception $e) { + return new JsonResponse([ + 'message' => $e->getMessage(), + ]); + } + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php old mode 100644 new mode 100755 diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php old mode 100644 new mode 100755 index 5b5848b..5966e7b --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php @@ -4,7 +4,9 @@ namespace App\Services; use App\Decorators\OcRegistryDecorator; use App\Exceptions\OrderValidationFailedException; +use Exception; use Openguru\OpenCartFramework\Config\Settings; +use Openguru\OpenCartFramework\Logger\Logger; use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface; use Openguru\OpenCartFramework\Telegram\TelegramService; use Rakit\Validation\Validator; @@ -17,19 +19,22 @@ class OrderCreateService private OcRegistryDecorator $oc; private Settings $settings; private TelegramService $telegramService; + private Logger $logger; public function __construct( ConnectionInterface $database, CartService $cartService, OcRegistryDecorator $registry, Settings $settings, - TelegramService $telegramService + TelegramService $telegramService, + Logger $logger ) { $this->database = $database; $this->cartService = $cartService; $this->oc = $registry; $this->settings = $settings; $this->telegramService = $telegramService; + $this->logger = $logger; } public function create(array $data, array $meta = []): void @@ -74,8 +79,10 @@ class OrderCreateService 'customer_group_id' => $customerGroupId, ]; + $orderId = null; + $this->database->transaction( - function () use ($orderData, $products, $totals, $orderStatusId, $now) { + function () use ($orderData, $products, $totals, $orderStatusId, $now, &$orderId) { $success = $this->database->insert(db_table('order'), $orderData); if (! $success) { @@ -156,22 +163,52 @@ class OrderCreateService $this->cartService->flush(); - $message = <<settings->get('telegram.chat_id'); + $template = $this->settings->get('telegram.owner_notification_template'); + $variables = [ + '{store_name}' => $orderData['store_name'], + '{order_id}' => $orderId, + '{customer}' => $orderData['firstname'] . ' ' . $orderData['lastname'], + '{email}' => $orderData['email'], + '{phone}' => $orderData['telephone'], + '{comment}' => $orderData['comment'], + '{address}' => $orderData['shipping_address_1'], + '{total}' => $total, + '{ip}' => $orderData['ip'], + '{created_at}' => $now, + ]; - $this->telegramService->sendMessage( - $this->settings->get('telegram.chat_id'), - sprintf( - $message, - $storeName, - $data['firstName'], - $data['lastName'], - $total, - ), - ); + if ($chatId && $template) { + $message = $this->telegramService->prepareMessage($template, $variables); + try { + $this->telegramService->sendMessage($chatId, $message); + } catch (Exception $exception) { + $this->logger->error( + 'Telegram sendMessage error: ' . json_encode([ + 'chat_id' => $chatId, + 'text' => $message, + ]) + ); + $this->logger->logException($exception); + } + } + + $customerChatId = $data['tgData']['id'] ?? null; + $template = $this->settings->get('telegram.customer_notification_template'); + if ($customerChatId && $template) { + $message = $this->telegramService->prepareMessage($template, $variables); + try { + $this->telegramService->sendMessage($customerChatId, $message); + } catch (Exception $exception) { + $this->logger->error( + 'Telegram sendMessage error: ' . json_encode([ + 'chat_id' => $chatId, + 'text' => $message, + ]) + ); + $this->logger->logException($exception); + } + } } private function validate(array $data): void diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php index 093a8ed..5dd7974 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php @@ -2,9 +2,9 @@ use App\Handlers\CategoriesHandler; use App\Handlers\CartHandler; -use App\Handlers\HelloWorldHandler; use App\Handlers\OrderHandler; use App\Handlers\ProductsHandler; +use App\Handlers\SettingsHandler; return [ 'products' => [ProductsHandler::class, 'handle'], @@ -15,4 +15,8 @@ return [ 'checkout' => [CartHandler::class, 'checkout'], 'getCart' => [CartHandler::class, 'index'], + + 'settings' => [SettingsHandler::class, 'index'], + 'manifest' => [SettingsHandler::class, 'manifest'], + 'testTgMessage' => [SettingsHandler::class, 'testTgMessage'], ]; diff --git a/spa/index.html b/spa/index.html index fb9591a..9738b41 100644 --- a/spa/index.html +++ b/spa/index.html @@ -4,10 +4,11 @@ - Vite + Vue + OpenCart Telegram Mini App
+
diff --git a/spa/src/App.vue b/spa/src/App.vue index 9d13aee..03e00e7 100644 --- a/spa/src/App.vue +++ b/spa/src/App.vue @@ -22,22 +22,21 @@ disableVerticalSwipes(); const router = useRouter(); const route = useRoute(); -const backButton = useBackButton(); watch( - () => route, + () => route.name, () => { if (route.name === 'home') { - backButton.hide?.(); + window.Telegram.WebApp.BackButton.hide(); + window.Telegram.WebApp.BackButton.offClick(); } else { - backButton.show?.(); - backButton.onClick?.(() => { + window.Telegram.WebApp.BackButton.show(); + window.Telegram.WebApp.BackButton.onClick(() => { window.Telegram.WebApp.HapticFeedback.impactOccurred('light'); router.back(); - }); } }, - {immediate: true, deep: true} + {immediate: true} ); diff --git a/spa/src/ApplicationError.vue b/spa/src/ApplicationError.vue new file mode 100644 index 0000000..e7ad6a2 --- /dev/null +++ b/spa/src/ApplicationError.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/spa/src/components/CartButton.vue b/spa/src/components/CartButton.vue index e3bffae..3356036 100644 --- a/spa/src/components/CartButton.vue +++ b/spa/src/components/CartButton.vue @@ -1,5 +1,5 @@ - - \ No newline at end of file diff --git a/spa/src/components/ProductsList.vue b/spa/src/components/ProductsList.vue index d8b31d7..5869abf 100644 --- a/spa/src/components/ProductsList.vue +++ b/spa/src/components/ProductsList.vue @@ -104,11 +104,8 @@ watch(() => route.params.id, async newId => { onMounted(async () => { const saved = productsStore.savedCategoryId === categoryId; - - console.log("Saved Category: ", saved); if (saved && productsStore.products.data.length > 0) { await nextTick(); - console.log("Products exists, scrolling to ", productsStore.savedScrollY); // повторяем до тех пор, пока высота не станет больше savedScrollY const interval = setInterval(() => { const maxScroll = document.documentElement.scrollHeight - window.innerHeight diff --git a/spa/src/main.js b/spa/src/main.js index 8eb5998..0575c07 100644 --- a/spa/src/main.js +++ b/spa/src/main.js @@ -1,9 +1,15 @@ -import { createApp } from 'vue' +import {createApp} from 'vue' import App from './App.vue' import './style.css' -import { VueTelegramPlugin } from 'vue-tg'; -import { router } from './router'; -import { createPinia } from 'pinia'; +import {VueTelegramPlugin} from 'vue-tg'; +import {router} from './router'; +import {createPinia} from 'pinia'; + +import {useCategoriesStore} from "@/stores/CategoriesStore.js"; +import {useSettingsStore} from "@/stores/SettingsStore.js"; +import ApplicationError from "@/ApplicationError.vue"; +import AppMetaInitializer from "@/utils/AppMetaInitializer.ts"; +import {injectYaMetrika} from "@/utils/yaMetrika.js"; const pinia = createPinia(); const app = createApp(App); @@ -12,22 +18,26 @@ app .use(router) .use(VueTelegramPlugin); -app.mount('#app'); - const settings = useSettingsStore(); const categoriesStore = useCategoriesStore(); categoriesStore.fetchTopCategories(); categoriesStore.fetchCategories(); -import {useCategoriesStore} from "@/stores/CategoriesStore.js"; -import {useSettingsStore} from "@/stores/SettingsStore.js"; - -if (settings.night_auto) { - window.Telegram.WebApp.onEvent('themeChanged', function () { - document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]); +settings.load() + .then(() => { + document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]); + if (settings.night_auto) { + window.Telegram.WebApp.onEvent('themeChanged', function () { + document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]); + }); + } + }) + .then(() => new AppMetaInitializer(settings).init()) + .then(() => app.mount('#app')) + .then(() => window.Telegram.WebApp.ready()) + .then(() => settings.ya_metrika_enabled && injectYaMetrika()) + .catch(error => { + console.error(error); + const errorApp = createApp(ApplicationError, {error}); + errorApp.mount('#app-error'); }); -} else { - document.documentElement.setAttribute('data-theme', settings.theme.light); -} - -window.Telegram.WebApp.ready(); diff --git a/spa/src/stores/CheckoutStore.js b/spa/src/stores/CheckoutStore.js index a6703a4..235c887 100644 --- a/spa/src/stores/CheckoutStore.js +++ b/spa/src/stores/CheckoutStore.js @@ -12,6 +12,7 @@ export const useCheckoutStore = defineStore('checkout', { phone: "+79999999999", address: "Москва, Красная площадь, 1", comment: "Доставить срочно❗️", + tgData: null, }, validationErrors: {}, @@ -26,6 +27,19 @@ export const useCheckoutStore = defineStore('checkout', { actions: { async makeOrder() { try { + const data = window.Telegram.WebApp.initDataUnsafe; + + if (! data.allows_write_to_pm) { + await window.Telegram.WebApp.requestWriteAccess((granted) => { + if (granted) { + console.log('Пользователь разрешил отправку сообщений'); + } else { + alert('Вы не дали разрешение — бот не сможет отправлять вам уведомления'); + } + }); + } + + this.customer.tgData = data.user; await storeOrder(this.customer); await window.Telegram.WebApp.HapticFeedback.notificationOccurred('success'); await useCartStore().getProducts(); @@ -44,6 +58,6 @@ export const useCheckoutStore = defineStore('checkout', { clearError(field) { this.validationErrors[field] = null; - } + }, }, }); diff --git a/spa/src/stores/SettingsStore.js b/spa/src/stores/SettingsStore.js index 36c2f20..7d675ef 100644 --- a/spa/src/stores/SettingsStore.js +++ b/spa/src/stores/SettingsStore.js @@ -1,12 +1,37 @@ import {defineStore} from "pinia"; +import {fetchSettings} from "@/utils/ftch.js"; export const useSettingsStore = defineStore('settings', { state: () => ({ + app_name: 'OpenCart Telegram магазин', + app_icon: '', + app_icon192: '', + app_icon180: '', + app_icon152: '', + app_icon120: '', + manifest_url: null, night_auto: true, + ya_metrika_enabled: false, theme: { light: 'light', dark: 'dark', }, noMoreProductsMessage: '🔚 Ну всё, разгрузили всё, что было. Даже кладовщика разбудить не удалось.', }), + + actions: { + async load() { + const settings = await fetchSettings(); + this.manifest_url = settings.manifest_url; + this.app_name = settings.app_name; + this.app_icon = settings.app_icon; + this.app_icon192 = settings.app_icon192; + this.app_icon180 = settings.app_icon180; + this.app_icon152 = settings.app_icon152; + this.app_icon120 = settings.app_icon120; + this.theme.light = settings.theme_light; + this.theme.dark = settings.theme_dark; + this.ya_metrika_enabled = settings.ya_metrika_enabled; + } + } }); diff --git a/spa/src/utils/AppMetaInitializer.ts b/spa/src/utils/AppMetaInitializer.ts new file mode 100644 index 0000000..b220f3f --- /dev/null +++ b/spa/src/utils/AppMetaInitializer.ts @@ -0,0 +1,46 @@ +class AppMetaInitializer { + private readonly settings: object; + + constructor(settings: object) { + this.settings = settings; + } + + public init() { + document.title = this.settings.app_name; + this.setMeta('application-name', this.settings.app_name); + this.setMeta('apple-mobile-web-app-title', this.settings.app_name); + this.setMeta('mobile-web-app-capable', 'yes'); + this.setMeta('apple-mobile-web-app-capable', 'yes'); + this.setMeta('apple-mobile-web-app-status-bar-style', 'default'); + this.setMeta('theme-color', '#000000'); + this.setMeta('msapplication-navbutton-color', '#000000'); + this.setMeta('apple-mobile-web-app-status-bar-style', 'black-translucent'); + this.addLink('manifest', this.settings.manifest_url); + + this.addLink('icon', this.settings.app_icon192, '192x192'); + this.addLink('apple-touch-icon', this.settings.app_icon192); + this.addLink('apple-touch-icon', this.settings.app_icon180, '180x180'); + this.addLink('apple-touch-icon', this.settings.app_icon152, '152x152'); + this.addLink('apple-touch-icon', this.settings.app_icon120, '120x120'); + } + + private setMeta(name: string, content: string) { + let meta = document.querySelector(`meta[name="${name}"]`); + if (!meta) { + meta = document.createElement('meta'); + meta.setAttribute('name', name); + document.head.appendChild(meta); + } + meta.setAttribute('content', content); + } + + private addLink(rel: string, href: string, sizes?: string) { + const link = document.createElement('link'); + link.rel = rel; + link.href = href; + if (sizes) link.sizes = sizes; + document.head.appendChild(link); + } +} + +export default AppMetaInitializer; \ No newline at end of file diff --git a/spa/src/utils/ftch.js b/spa/src/utils/ftch.js index 41e0a90..9d5a4b0 100644 --- a/spa/src/utils/ftch.js +++ b/spa/src/utils/ftch.js @@ -59,4 +59,8 @@ export async function cartEditItem(data) { }); } +export async function fetchSettings() { + return await ftch('settings'); +} + export default ftch; diff --git a/spa/src/utils/yaMetrika.js b/spa/src/utils/yaMetrika.js new file mode 100644 index 0000000..a59c282 --- /dev/null +++ b/spa/src/utils/yaMetrika.js @@ -0,0 +1,7 @@ +export function injectYaMetrika() { + const script = document.createElement('script'); + script.src = '/index.php?route=extension/tgshop/handle/ya_metrika'; + script.async = true; + document.head.appendChild(script); + console.log('Yandex Metrika injected to the page.'); +} \ No newline at end of file