feat(orders): tg notifications, ya metrika, meta tags
This commit is contained in:
345
module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php
Executable file
345
module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php
Executable file
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
class ControllerExtensionModuleTgshop extends Controller
|
||||
{
|
||||
private static array $themes = [
|
||||
'light' => 'Светлая (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' => <<<TEXT
|
||||
Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись
|
||||
под иконкой, если пользователь добавит приложение на главный экран своего устройства.
|
||||
Рекомендуется короткое и понятное название (до 20 символов).
|
||||
TEXT,
|
||||
],
|
||||
|
||||
'module_tgshop_app_icon' => [
|
||||
'hidden' => true,
|
||||
'type' => 'image',
|
||||
'help' => <<<TEXT
|
||||
Изображение, которое будет отображаться в Telegram Mini App и на рабочем столе устройства,
|
||||
если пользователь добавит приложение как ярлык. Используйте квадратное изображение PNG или SVG,
|
||||
размером не менее 192×192 пикселей, а лучше 512x512.
|
||||
TEXT,
|
||||
],
|
||||
|
||||
'module_tgshop_theme_light' => [
|
||||
'type' => 'select',
|
||||
'options' => static::$themes,
|
||||
'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для дневного режима. <a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">Посмотреть как выглядят темы</a>',
|
||||
],
|
||||
|
||||
'module_tgshop_theme_dark' => [
|
||||
'type' => 'select',
|
||||
'options' => static::$themes,
|
||||
'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для ночного режима. <a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">Посмотреть как выглядят темы</a>',
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'module_tgshop_bot_token' => [
|
||||
'type' => 'text',
|
||||
'placeholder' => 'Введите токен от телеграм бота',
|
||||
'help' => <<<TEXT
|
||||
Токен, полученный при создании бота через @BotFather.
|
||||
Он используется для взаимодействия модуля с Telegram API.
|
||||
TEXT,
|
||||
],
|
||||
'module_tgshop_chat_id' => [
|
||||
'type' => 'chatid',
|
||||
'placeholder' => 'Введите Chat ID',
|
||||
'help' => <<<TEXT
|
||||
Идентификатор Telegram-чата, куда будут отправляться уведомления о новых заказах.
|
||||
Если оставить поле пустым, уведомления отправляться не будут.
|
||||
TEXT,
|
||||
],
|
||||
'module_tgshop_owner_notification_template' => [
|
||||
'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' => [
|
||||
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
// Heading
|
||||
$_['heading_title'] = 'Telegram Магазин';
|
||||
|
||||
$_['text_module'] = 'Модули';
|
||||
$_['text_success'] = 'Настройки успешно изменены!';
|
||||
$_['text_edit'] = 'Настройки';
|
||||
|
||||
$_['tab_telegram'] = 'Telegram';
|
||||
$_['tab_statistics'] = 'Статистика';
|
||||
$_['tab_shop'] = 'Магазин';
|
||||
$_['tab_orders'] = 'Заказы';
|
||||
|
||||
$_['lbl_module_tgshop_status'] = 'Статус';
|
||||
$_['lbl_module_tgshop_app_name'] = 'Название приложения';
|
||||
$_['lbl_module_tgshop_app_icon'] = 'Иконка приложения';
|
||||
$_['lbl_module_tgshop_bot_token'] = 'Telegram Bot Token';
|
||||
$_['lbl_module_tgshop_chat_id'] = 'Chat ID для уведомлений';
|
||||
$_['lbl_module_tgshop_owner_notification_template'] = 'Шаблон уведомления о новом заказе владельцу';
|
||||
$_['lbl_module_tgshop_customer_notification_template'] = 'Шаблон уведомления о новом заказе покупателю';
|
||||
$_['lbl_module_tgshop_yandex_metrika'] = 'Код счётчика Яндекс Метрики';
|
||||
$_['lbl_module_tgshop_theme_light'] = 'Светлая тема';
|
||||
$_['lbl_module_tgshop_theme_dark'] = 'Тёмная тема';
|
||||
$_['lbl_module_tgshop_mainpage_products'] = 'Товары на главной';
|
||||
$_['lbl_module_tgshop_featured_products'] = 'Избранные товары';
|
||||
|
||||
// Entry
|
||||
$_['entry_status'] = 'Статус';
|
||||
|
||||
// Error
|
||||
$_['error_permission'] = 'У вас недостаточно прав для внесения изменений!';
|
||||
324
module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop.twig
Executable file
324
module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop.twig
Executable file
@@ -0,0 +1,324 @@
|
||||
{{ header }}{{ column_left }}
|
||||
<div id="content">
|
||||
<div class="page-header">
|
||||
<div class="container-fluid">
|
||||
<div class="pull-right">
|
||||
<button type="submit" form="form-module" data-toggle="tooltip" title="{{ button_save }}"
|
||||
class="btn btn-primary"><i class="fa fa-save"></i></button>
|
||||
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i
|
||||
class="fa fa-reply"></i></a></div>
|
||||
<h1>{{ heading_title }}</h1>
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb in breadcrumbs %}
|
||||
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
{% if error_warning %}
|
||||
<div class="alert alert-danger alert-dismissible"><i
|
||||
class="fa fa-exclamation-circle"></i> {{ error_warning }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible"><i class="fa fa-check-circle"></i> {{ success }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-pencil"></i> {{ text_edit }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-module"
|
||||
class="form-horizontal">
|
||||
|
||||
<pre>
|
||||
* Проверка request от телеграм
|
||||
4. Выбор товаров, которые будут отображаться на главной странице
|
||||
1. Шаблон для уведомлений покупателя о заказе
|
||||
2. Требовать ввод email/phone
|
||||
3. Группа покупателей для заказов от ТГ
|
||||
</pre>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{% for tabKey, tabItems in settings %}
|
||||
<li{% if tabKey == 'general' %} class="active" {% endif %}>
|
||||
<a href="#{{ tabKey }}" data-toggle="tab">
|
||||
{% if attribute(_context, 'tab_' ~ tabKey) %}
|
||||
{{ attribute(_context, 'tab_' ~ tabKey) }}
|
||||
{% else %}
|
||||
{{ 'tab_' ~ tabKey }}
|
||||
{% endif %}
|
||||
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
{% for tabKey, tabItems in settings %}
|
||||
<div class="tab-pane{%if tabKey == 'general' %} active{% endif %}" id="{{ tabKey }}">
|
||||
{% for settingKey, item in tabItems %}
|
||||
<div class="form-group{%if item['required'] %} required{% endif %}{% if item['hidden'] %} hidden{% endif %}">
|
||||
<label class="col-sm-2 control-label" for="{{ settingKey }}">
|
||||
{% if attribute(_context, 'lbl_' ~ settingKey) %}
|
||||
{{ attribute(_context, 'lbl_' ~ settingKey) }}
|
||||
{% else %}
|
||||
{{ 'lbl_' ~ settingKey }}
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
{# Select #}
|
||||
{% if item['type'] == 'select' %}
|
||||
<select name="{{ settingKey }}" id="{{ settingKey }}" class="form-control">
|
||||
{% for key, value in item['options'] %}
|
||||
<option value="{{ key }}" {% if key == attribute(_context, settingKey) %}selected="selected"{% endif %}>
|
||||
{{ value }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{# Text Input #}
|
||||
{% elseif item['type'] == 'text' %}
|
||||
<input type="text"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
/>
|
||||
|
||||
{# Image #}
|
||||
{% elseif item['type'] == 'image' %}
|
||||
<a href="" id="thumb-image" data-toggle="image" class="img-thumbnail">
|
||||
<img src="{{ attribute(_context, settingKey) }}"
|
||||
data-placeholder="{{ attribute(_context, settingKey) }}"
|
||||
/>
|
||||
</a>
|
||||
<input type="hidden"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
id="{{ settingKey }}"
|
||||
/>
|
||||
|
||||
{# Image #}
|
||||
{% elseif item['type'] == 'textarea' %}
|
||||
<textarea name="{{ settingKey }}"
|
||||
rows="{{ item['rows'] }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
>{{ attribute(_context, settingKey) }}</textarea>
|
||||
|
||||
{# Products #}
|
||||
{% elseif item['type'] == 'products' %}
|
||||
<input type="text" value="" placeholder="Начните вводить название товара..." id="{{ settingKey }}-input" class="form-control"/>
|
||||
<div id="{{ settingKey }}-list" class="well well-sm" style="height: 150px; overflow: auto;">
|
||||
{% for product in attribute(_context, settingKey) %}
|
||||
<div id="{{ settingKey }}-{{ product.product_id }}">
|
||||
<i class="fa fa-minus-circle"></i> {{ product.name }}
|
||||
<input type="hidden" name="{{ settingKey }}[]" value="{{ product.product_id }}"/>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
$('#{{ settingKey }}-input').autocomplete({
|
||||
'source': function(request, response) {
|
||||
$.ajax({
|
||||
url: 'index.php?route=catalog/product/autocomplete&user_token={{ user_token }}&filter_name=' + encodeURIComponent(request),
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
response($.map(json, function(item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: item['product_id']
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
'select': function(item) {
|
||||
$('#{{ settingKey }}').val('');
|
||||
$('#{{ settingKey }}-' + item['value']).remove();
|
||||
$('#{{ settingKey }}-list').append('<div id="{{ settingKey }}-' + item['value'] + '"><i class="fa fa-minus-circle"></i> ' + item['label'] + '<input type="hidden" name="{{ settingKey }}[]" value="' + item['value'] + '" /></div>');
|
||||
}
|
||||
});
|
||||
|
||||
$('#{{ settingKey }}-list').delegate('.fa-minus-circle', 'click', function() {
|
||||
$(this).parent().remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
{# ChatID #}
|
||||
{% elseif item['type'] == 'chatid' %}
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button id="{{ settingKey }}-btn" class="btn btn-primary" type="button">
|
||||
<i class="fa fa-refresh"></i> Получить Chat ID
|
||||
</button>
|
||||
</span>
|
||||
<input type="text"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
/>
|
||||
<script>
|
||||
$('#{{ settingKey }}-btn').click(function () {
|
||||
const telegramToken = $('#module_tgshop_bot_token').val(); // fetch from input
|
||||
if (! telegramToken) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`https://api.telegram.org/bot${telegramToken}/getUpdates`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data.ok || !data.result.length) {
|
||||
alert('Не удалось получить обновления от бота. Убедитесь, что вы написали боту сообщение.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ищем последнее сообщение с chat_id
|
||||
const lastMessage = data.result.reverse().find(update => update.message && update.message.chat);
|
||||
if (!lastMessage) {
|
||||
alert('Не найдено сообщений с chat_id.');
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = lastMessage.message.chat.id;
|
||||
$('#{{ settingKey }}').val(chatId); // подставляем в поле
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Ошибка при получении chat_id. Проверьте токен и соединение.');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-link btn-xs" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse" aria-expanded="false" aria-controls="collapseExample">
|
||||
Инструкция как получить ChatID.
|
||||
</button>
|
||||
<div class="collapse" id="{{ settingKey }}-collapse">
|
||||
<div class="well">
|
||||
<p class="text-primary">Как получить Chat ID</p>
|
||||
<ol>
|
||||
<li>Убедитесь, что вы ввели Telegram Bot Token выше.</li>
|
||||
<li>Откройте вашего бота в Telegram и отправьте ему любое сообщение.</li>
|
||||
<li>Вернитесь сюда и нажмите кнопку «Получить Chat ID» — мы автоматически подставим его в поле ниже.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elseif item['type'] == 'tg_message_template' %}
|
||||
<div style="margin-bottom: 10px;">
|
||||
<textarea name="{{ settingKey }}"
|
||||
rows="{{ item['rows'] }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
>{{ attribute(_context, settingKey) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse">
|
||||
Документация
|
||||
</button>
|
||||
<button id="{{ settingKey }}-btn-test" type="button" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-envelope"></i>
|
||||
Отправить тестовое уведомление
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse" id="{{ settingKey }}-collapse" style="margin-top: 15px">
|
||||
<div class="well">
|
||||
<p>Вы можете использовать переменные:</p>
|
||||
<ul>
|
||||
<li><code>{store_name}</code> — название магазина</li>
|
||||
<li><code>{order_id}</code> — номер заказа</li>
|
||||
<li><code>{customer}</code> — имя и фамилия покупателя</li>
|
||||
<li><code>{email}</code> — email покупателя</li>
|
||||
<li><code>{phone}</code> — телефон</li>
|
||||
<li><code>{comment}</code> — комментарий к заказу</li>
|
||||
<li><code>{address}</code> — адрес доставки</li>
|
||||
<li><code>{total}</code> — сумма заказа</li>
|
||||
<li><code>{ip}</code> — IP покупателя</li>
|
||||
<li><code>{created_at}</code> — дата и время создания заказа</li>
|
||||
</ul>
|
||||
<p>Форматирование: поддерживается <a href="https://core.telegram.org/bots/api#markdownv2-style" target="_blank">*MarkdownV2* <i class="fa fa-external-link"></i></a>.</p>
|
||||
<p>Символы, которые нужно экранировать в тексте:</p>
|
||||
<pre>_ * [ ] ( ) ~ ` > # + - = | { } . !</pre>
|
||||
<p>Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования. Например вместо <code>Заказ #123</code> нужно писать <code>Заказ \#123</code>.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('#{{ settingKey }}-btn-test').click(function () {
|
||||
const telegramToken = $('#module_tgshop_bot_token').val(); // fetch from input
|
||||
if (! telegramToken) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = $('#module_tgshop_chat_id').val(); // fetch from input
|
||||
if (! chatId) {
|
||||
alert('Сначала введите Chat ID!');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $('#{{ settingKey }}').val();
|
||||
if (! template) {
|
||||
alert('Сначала задайте шаблон!');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/index.php?route=extension/tgshop/handle&api_action=testTgMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: telegramToken,
|
||||
chat_id: chatId,
|
||||
template: template
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
alert(response.message || 'Уведомление успешно отправлено');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
alert('Ошибка при отправке тестового сообщения');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
Unsupported {{ item|json_encode }}
|
||||
{% endif %}
|
||||
|
||||
{% if attribute(_context, 'error_' ~ settingKey) %}
|
||||
<div class="text-danger">{{ attribute(_context, 'error_' ~ settingKey) }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item['help'] %}
|
||||
<p class="help-block">{{ item['help'] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ footer }}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{ header }}{{ column_left }}
|
||||
<div id="content">
|
||||
<div class="page-header">
|
||||
<div class="container-fluid">
|
||||
<div class="pull-right">
|
||||
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i
|
||||
class="fa fa-reply"></i></a></div>
|
||||
<h1>{{ heading_title }}</h1>
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb in breadcrumbs %}
|
||||
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
{% if error_warning %}
|
||||
<div class="alert alert-danger alert-dismissible"><i
|
||||
class="fa fa-exclamation-circle"></i> {{ error_warning }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-pencil"></i> Инициализация модуля</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="col-md-push-4 col-md-4 text-center">
|
||||
<h2>Инициализация модуля</h2>
|
||||
<p>Модуль запускается первый раз, поэтому необходимо инициализировать настройки для его корректной работы.</p>
|
||||
<form action="{{ action }}" method="post" enctype="multipart/form-data" class="form-horizontal">
|
||||
<button type="submit"
|
||||
data-toggle="tooltip"
|
||||
title="Нажмите чтобы выполнить начальную инициализацию модуля"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Инициализация
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ footer }}
|
||||
@@ -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) {
|
||||
// Убираем <noscript>...</noscript>
|
||||
$input = preg_replace('#<noscript>.*?</noscript>#is', '', $input);
|
||||
|
||||
// Убираем <!-- комментарии -->
|
||||
$input = preg_replace('#<!--.*?-->#s', '', $input);
|
||||
|
||||
// Извлекаем содержимое <script>...</script>
|
||||
if (preg_match('#<script[^>]*>(.*?)</script>#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;
|
||||
}
|
||||
}
|
||||
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/.env
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/.env
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/.env.example
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/.env.example
Normal file → Executable file
@@ -42,4 +42,9 @@ class Settings
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function getHash(): string
|
||||
{
|
||||
return md5(serialize($this->settings));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
51
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramService.php
Normal file → Executable file
51
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramService.php
Normal file → Executable file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramServiceProvider.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramServiceProvider.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/src/Decorators/OcRegistryDecorator.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/src/Decorators/OcRegistryDecorator.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php
Normal file → Executable file
@@ -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');
|
||||
|
||||
120
module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php
Executable file
120
module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/SettingsHandler.php
Executable file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
|
||||
use Openguru\OpenCartFramework\Router\Router;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||
|
||||
class SettingsHandler
|
||||
{
|
||||
private Settings $settings;
|
||||
private ImageToolInterface $imageTool;
|
||||
private Router $router;
|
||||
private TelegramService $telegramService;
|
||||
|
||||
public function __construct(Settings $settings, ImageToolInterface $imageTool, Router $router, TelegramService $telegramService)
|
||||
{
|
||||
$this->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php
Normal file → Executable file
71
module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php
Normal file → Executable file
71
module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php
Normal file → Executable file
@@ -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 = <<<MARKDOWN
|
||||
*Новый заказ в магазине %s*
|
||||
Покупатель: %s %s
|
||||
Сумма: %s
|
||||
MARKDOWN;
|
||||
$chatId = $this->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
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Vite + Vue</title>
|
||||
<title>OpenCart Telegram Mini App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app-error"></div>
|
||||
<script src="https://telegram.org/js/telegram-web-app.js?58"></script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -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}
|
||||
);
|
||||
</script>
|
||||
|
||||
21
spa/src/ApplicationError.vue
Normal file
21
spa/src/ApplicationError.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div style="z-index: 99999" class="fixed top-0 left-0 w-full h-full bg-base-100">
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-20">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
<h1 class="font-semibold text-2xl mb-2">Магазин временно недоступен</h1>
|
||||
<p class="text-sm text-muted">Мы на перерыве, скоро всё снова заработает 🛠️</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
error: Error,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="route.name !== 'cart.show'" class="fixed right-2 bottom-30 z-50 opacity-90">
|
||||
<div v-if="isCartBtnShow" class="fixed right-2 bottom-30 z-50 opacity-90">
|
||||
<div class="indicator">
|
||||
<span class="indicator-item indicator-top indicator-start badge badge-secondary">{{ cart.productsCount }}</span>
|
||||
<button class="btn btn-primary btn-lg btn-circle" @click="openCart">
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted} from "vue";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useCartStore} from "@/stores/CartStore.js";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
@@ -23,6 +23,11 @@ const cart = useCartStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const isCartBtnShow = computed(() => {
|
||||
return route.name !== 'cart.show' && route.name !== 'checkout';
|
||||
});
|
||||
|
||||
|
||||
function openCart() {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
router.push({name: 'cart.show'});
|
||||
|
||||
@@ -11,7 +11,3 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
46
spa/src/utils/AppMetaInitializer.ts
Normal file
46
spa/src/utils/AppMetaInitializer.ts
Normal file
@@ -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;
|
||||
@@ -59,4 +59,8 @@ export async function cartEditItem(data) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchSettings() {
|
||||
return await ftch('settings');
|
||||
}
|
||||
|
||||
export default ftch;
|
||||
|
||||
7
spa/src/utils/yaMetrika.js
Normal file
7
spa/src/utils/yaMetrika.js
Normal file
@@ -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.');
|
||||
}
|
||||
Reference in New Issue
Block a user