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 index b81e5d5..1676847 100755 --- a/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php +++ b/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php @@ -547,17 +547,18 @@ HTML, ], 'orders' => [ - 'module_tgshop_order_customer_group_id' => [ - 'type' => 'select', - 'options' => $this->getCustomerGroups(), - 'help' => 'Группа покупателей, которая будет назначена для заказов, оформленных через Telegram-магазин.', - ], - 'module_tgshop_order_default_status_id' => [ 'type' => 'select', 'options' => $this->getOrderStatuses(), 'help' => 'Статус, с которым будут создаваться заказы через Telegram по умолчанию.', ], + + 'module_tgshop_order_customer_group_id' => [ + 'hidden' => true, + 'type' => 'select', + 'options' => $this->getCustomerGroups(), + 'help' => 'Группа покупателей, которая будет назначена для заказов, оформленных через Telegram-магазин.', + ], ], ]; } 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 3a64c76..9d2717b 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 @@ -45,8 +45,7 @@ class ControllerExtensionTgshopHandle extends Controller 'app_debug' => $appDebug, 'oc_config_tax' => $this->config->get('config_tax'), 'oc_default_currency' => $this->config->get('config_currency'), - // ID группы покупателей, которая будет использоаваться в заказах через Телеграм. - 'oc_customer_group_id' => $this->config->get('module_tgshop_order_customer_group_id'), + 'oc_customer_group_id' => $this->config->get('config_customer_group_id'), // ID магазина, для которого будут создаваться заказы из Телеграм 'oc_store_id' => 0, // Название магазина, для которого будут создаваться заказы из Телеграм 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 index 13c9b2d..512ea12 100755 --- 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 @@ -177,6 +177,7 @@ class OrderCreateService $this->cartService->flush(); $orderData['order_id'] = $orderId; + $orderData['total_numeric'] = $orderData['total'] ?? 0; $orderData['total'] = $cart['total_text'] ?? ''; $this->sendNotifications($orderData, $data['tgData']); @@ -192,6 +193,8 @@ class OrderCreateService 'id' => $orderData['order_id'], 'created_at' => $dateTimeFormatted, 'total' => $orderData['total'], + 'final_total_numeric' => $orderData['total_numeric'], + 'currency' => $currencyCode, ]; } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php index f38ef5c..fcfce52 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php @@ -214,6 +214,7 @@ class ProductsService $configTax = $this->oc->config->get('config_tax'); $product_info = $this->oc->model_catalog_product->getProduct($productId); + $currency = $this->oc->session->data['currency']; if (! $product_info) { throw new EntityNotFoundException('Product with id ' . $productId . ' not found'); @@ -280,24 +281,24 @@ class ProductsService $data['images'] = $images; - $data['price'] = $this->currency->format( - $this->tax->calculate( - $product_info['price'], - $product_info['tax_class_id'], - $configTax, - ), - $this->oc->session->data['currency'] + $productPrice = $this->tax->calculate( + $product_info['price'], + $product_info['tax_class_id'], + $configTax, ); + $data['price'] = $this->currency->format($productPrice, $currency); + $data['currency'] = $currency; + $data['final_price_numeric'] = $productPrice; if (! is_null($product_info['special']) && (float) $product_info['special'] >= 0) { - $data['special'] = $this->currency->format( - $this->tax->calculate( - $product_info['special'], - $product_info['tax_class_id'], - $configTax, - ), - $this->oc->session->data['currency'] + $productSpecialPrice = $this->tax->calculate( + $product_info['special'], + $product_info['tax_class_id'], + $configTax, ); + + $data['special'] = $this->currency->format($productSpecialPrice, $currency); + $data['final_price_numeric'] = $productSpecialPrice; $tax_price = (float) $product_info['special']; } else { $data['special'] = false; @@ -305,7 +306,7 @@ class ProductsService } if ($configTax) { - $data['tax'] = $this->currency->format($tax_price, $this->oc->session->data['currency']); + $data['tax'] = $this->currency->format($tax_price, $currency); } else { $data['tax'] = false; } @@ -323,7 +324,7 @@ class ProductsService $product_info['tax_class_id'], $configTax, ), - $this->oc->session->data['currency'] + $currency ) ); } @@ -341,7 +342,7 @@ class ProductsService $product_info['tax_class_id'], $configTax ? 'P' : false ), - $this->oc->session->data['currency'] + $currency ); $product_option_value_data[] = array( diff --git a/spa/src/constants/yaMetrikaGoals.js b/spa/src/constants/yaMetrikaGoals.js new file mode 100644 index 0000000..cdc557e --- /dev/null +++ b/spa/src/constants/yaMetrikaGoals.js @@ -0,0 +1,14 @@ +export const YA_METRIKA_GOAL = { + ADD_TO_CART: 'add_to_cart', + CREATE_ORDER: 'create_order', + ORDER_CREATED_SUCCESS: 'order_created_success', + VIEW_PRODUCT: 'view_product', + VIEW_CART: 'view_cart', + VIEW_CHECKOUT: 'view_checkout', + VIEW_HOME: 'view_home', + VIEW_FILTERS: 'view_filters', + FILTERS_APPLY: 'filters_apply', + FILTERS_RESET: 'filters_reset', + VIEW_SEARCH: 'view_search', + PERFORM_SEARCH: 'perform_search', +}; diff --git a/spa/src/router.js b/spa/src/router.js index 2ef525c..779ba49 100644 --- a/spa/src/router.js +++ b/spa/src/router.js @@ -35,3 +35,9 @@ export const router = createRouter({ history: createWebHashHistory('/image/catalog/tgshopspa/'), routes, }); + +router.beforeEach((to, from, next) => { + const ym = useYaMetrikaStore(); + ym.prevPath = from.path; + next(); +}); diff --git a/spa/src/stores/CategoriesStore.js b/spa/src/stores/CategoriesStore.js index 3c7ed72..288b31c 100644 --- a/spa/src/stores/CategoriesStore.js +++ b/spa/src/stores/CategoriesStore.js @@ -38,5 +38,24 @@ export const useCategoriesStore = defineStore('categories', { this.isLoading = false; } }, + + async findCategoryById(id, list = []) { + if (! id) return null; + + if (list && list.length === 0) { + await this.fetchCategories(); + list = this.categories; + } + + for (const cat of list) { + if (parseInt(cat.id) === parseInt(id)) return cat; + if (cat.children?.length) { + const found = await this.findCategoryById(id, cat.children); + if (found) return found; + } + } + + return null; + } }, }); diff --git a/spa/src/stores/SearchStore.js b/spa/src/stores/SearchStore.js index eb0973f..68aa0c3 100644 --- a/spa/src/stores/SearchStore.js +++ b/spa/src/stores/SearchStore.js @@ -1,5 +1,7 @@ import {defineStore} from "pinia"; import ftch from "@/utils/ftch.js"; +import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; export const useSearchStore = defineStore('search', { state: () => ({ @@ -31,6 +33,10 @@ export const useSearchStore = defineStore('search', { return this.reset(); } + useYaMetrikaStore().reachGoal(YA_METRIKA_GOAL.PERFORM_SEARCH, { + keyword: this.search, + }); + try { this.isLoading = true; this.products = await ftch('products', { diff --git a/spa/src/stores/yaMetrikaStore.js b/spa/src/stores/yaMetrikaStore.js index 5df1ebc..5fe2074 100644 --- a/spa/src/stores/yaMetrikaStore.js +++ b/spa/src/stores/yaMetrikaStore.js @@ -1,9 +1,12 @@ import {defineStore} from "pinia"; import {useSettingsStore} from "@/stores/SettingsStore.js"; +import sha256 from 'crypto-js/sha256'; +import {toRaw} from "vue"; export const useYaMetrikaStore = defineStore('ya_metrika', { state: () => ({ queue: [], + prevPath: null, }), actions: { @@ -16,6 +19,8 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { const fullUrl = `/#${url}`; + params.referer = params.referer ?? this.prevPath; + if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) { console.debug('[ym] Hit ', fullUrl); console.debug('[ym] ID ', window.YA_METRIKA_ID); @@ -33,6 +38,49 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { } }, + reachGoal(target, params = {}) { + const settings = useSettingsStore(); + if (!settings.ya_metrika_enabled) { + console.debug('[ym] Yandex Metrika disabled in settings.'); + return; + } + + if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) { + console.debug('[ym] reachGoal ', target, ' params: ', params); + window.ym(window.YA_METRIKA_ID, 'reachGoal', target, params); + } else { + console.debug('[ym] Yandex Metrika is not initialized. Pushed to queue.'); + this.queue.push({ + event: 'reachGoal', + payload: { + target, + params + }, + }); + } + }, + + initUserParams() { + if (typeof window.ym === 'function' && window.YA_METRIKA_ID !== undefined) { + let tgID = null; + + if (window?.Telegram?.WebApp?.initDataUnsafe?.user?.id) { + tgID = sha256(window.Telegram.WebApp.initDataUnsafe.user.id).toString(); + } + + const userParams = { + tg_id: tgID, + language: window.Telegram?.WebApp?.initDataUnsafe?.user?.language_code || 'unknown', + platform: window.Telegram?.WebApp?.platform || 'unknown', + }; + + window.ym(window.YA_METRIKA_ID, 'userParams', userParams); + console.debug('[ym] User params initialized: ', userParams); + } else { + console.debug('[ym] Yandex Metrika is not initialized. Could not init user params.'); + } + }, + processQueue() { if (this.queue.length === 0) { return; @@ -43,8 +91,10 @@ export const useYaMetrikaStore = defineStore('ya_metrika', { while (this.queue.length > 0) { const item = this.queue.shift(); if (item.event === 'hit') { - console.debug('[ym] Queue ', item); + console.debug('[ym] Queue ', toRaw(item)); window.ym(window.YA_METRIKA_ID, item.event, item.payload.url, item.payload.params); + } else if (item.event === 'reachGoal') { + window.ym(window.YA_METRIKA_ID, item.event, item.payload.target, item.payload.params); } else { console.error('[ym] Unsupported queue event: ', item.event); } diff --git a/spa/src/utils/yaMetrika.js b/spa/src/utils/yaMetrika.js index 3a516ee..ca9cb80 100644 --- a/spa/src/utils/yaMetrika.js +++ b/spa/src/utils/yaMetrika.js @@ -23,6 +23,7 @@ export function injectYaMetrika() { window.YA_METRIKA_ID = getMetrikaId(); console.debug('[Init] Detected Yandex.Metrika ID:', window.YA_METRIKA_ID); const yaMetrika = useYaMetrikaStore(); + yaMetrika.initUserParams(); yaMetrika.processQueue(); } } diff --git a/spa/src/views/Cart.vue b/spa/src/views/Cart.vue index 621fe2e..7b07744 100644 --- a/spa/src/views/Cart.vue +++ b/spa/src/views/Cart.vue @@ -139,9 +139,13 @@ import OptionRadio from "@/components/ProductOptions/Cart/OptionRadio.vue"; import OptionCheckbox from "@/components/ProductOptions/Cart/OptionCheckbox.vue"; import OptionText from "@/components/ProductOptions/Cart/OptionText.vue"; import {computed, onMounted} from "vue"; -import {useRouter} from "vue-router"; +import {useRoute, useRouter} from "vue-router"; import {useSettingsStore} from "@/stores/SettingsStore.js"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; +const route = useRoute(); +const yaMetrika = useYaMetrikaStore(); const cart = useCartStore(); const router = useRouter(); const settings = useSettingsStore(); @@ -169,11 +173,15 @@ function goToCheckout() { onMounted(async () => { window.document.title = 'Корзина покупок'; + yaMetrika.pushHit(route.path, { + title: 'Корзина покупок', + }); + yaMetrika.reachGoal(YA_METRIKA_GOAL.VIEW_CART); }); \ No newline at end of file diff --git a/spa/src/views/CategoriesList.vue b/spa/src/views/CategoriesList.vue index 759e0f7..a359897 100644 --- a/spa/src/views/CategoriesList.vue +++ b/spa/src/views/CategoriesList.vue @@ -58,9 +58,11 @@ import {router} from "@/router.js"; import {useCategoriesStore} from "@/stores/CategoriesStore.js"; import {useRoute} from "vue-router"; import CategoryItem from "@/components/CategoriesList/CategoryItem.vue"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; const route = useRoute(); const categoriesStore = useCategoriesStore(); +const yaMetrika = useYaMetrikaStore(); const parentId = computed(() => route.params.id ? Number(route.params.id) : null); @@ -106,6 +108,9 @@ function showProductsInParentCategory() { onMounted(async () => { window.document.title = 'Каталог'; + yaMetrika.pushHit(route.path, { + title: 'Каталог', + }); await categoriesStore.fetchCategories(); }); diff --git a/spa/src/views/Checkout.vue b/spa/src/views/Checkout.vue index 90cc494..1220559 100644 --- a/spa/src/views/Checkout.vue +++ b/spa/src/views/Checkout.vue @@ -69,11 +69,15 @@ import {useCheckoutStore} from "@/stores/CheckoutStore.js"; import TgInput from "@/components/Form/TgInput.vue"; import TgTextarea from "@/components/Form/TgTextarea.vue"; -import {useRouter} from "vue-router"; +import {useRoute, useRouter} from "vue-router"; import {computed, onMounted, ref} from "vue"; import {IMaskComponent} from "vue-imask"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; const checkout = useCheckoutStore(); +const yaMetrika = useYaMetrikaStore(); +const route = useRoute(); const router = useRouter(); const error = ref(null); @@ -84,7 +88,15 @@ const btnText = computed(() => { async function onCreateBtnClick() { try { error.value = null; + yaMetrika.reachGoal(YA_METRIKA_GOAL.CREATE_ORDER, { + price: checkout.order?.final_total_numeric, + currency: checkout.order?.currency, + }); await checkout.makeOrder(); + yaMetrika.reachGoal(YA_METRIKA_GOAL.ORDER_CREATED_SUCCESS, { + price: checkout.order?.final_total_numeric, + currency: checkout.order?.currency, + }); router.push({name: 'order_created'}); } catch { error.value = 'Невозможно создать заказ.'; @@ -93,5 +105,9 @@ async function onCreateBtnClick() { onMounted(async () => { window.document.title = 'Оформление заказа'; + yaMetrika.pushHit(route.path, { + title: 'Оформление заказа', + }); + yaMetrika.reachGoal(YA_METRIKA_GOAL.VIEW_CHECKOUT); }); diff --git a/spa/src/views/Filters.vue b/spa/src/views/Filters.vue index 4233228..2487fc8 100644 --- a/spa/src/views/Filters.vue +++ b/spa/src/views/Filters.vue @@ -49,8 +49,10 @@ import {nextTick, onMounted} from "vue"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import ProductPrice from "@/components/ProductFilters/Components/ProductPrice.vue"; import ForMainPage from "@/components/ProductFilters/Components/ForMainPage.vue"; -import {useRouter} from "vue-router"; +import {useRoute, useRouter} from "vue-router"; import ProductCategory from "@/components/ProductFilters/Components/ProductCategory/ProductCategory.vue"; +import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; defineOptions({ name: 'Filters' @@ -63,6 +65,8 @@ const componentMap = { }; const router = useRouter(); +const yaMetrika = useYaMetrikaStore(); +const route = useRoute(); const emit = defineEmits(['close', 'apply']); const filtersStore = useProductFiltersStore(); @@ -72,6 +76,7 @@ const applyFilters = async () => { filtersStore.applied = JSON.parse(JSON.stringify(filtersStore.draft)); console.debug('Filters: apply filters. Hash for router: ', filtersStore.paramsHashForRouter); haptic.impactOccurred('soft'); + yaMetrika.reachGoal(YA_METRIKA_GOAL.FILTERS_APPLY); await nextTick(); router.back(); } @@ -80,6 +85,7 @@ const resetFilters = async () => { filtersStore.applied = filtersStore.default; console.debug('Filters: reset filters. Hash for router: ', filtersStore.paramsHashForRouter); haptic.notificationOccurred('success'); + yaMetrika.reachGoal(YA_METRIKA_GOAL.FILTERS_RESET); await nextTick(); window.scrollTo(0, 0); router.back(); @@ -89,6 +95,9 @@ onMounted(async () => { console.debug('Filters: OnMounted'); window.document.title = 'Фильтры'; + yaMetrika.pushHit(route.path, {title: 'Фильтры'}); + yaMetrika.reachGoal(YA_METRIKA_GOAL.VIEW_FILTERS); + if (filtersStore.applied?.rules) { console.debug('Filters: Found applied filters.'); filtersStore.draft = JSON.parse(JSON.stringify(filtersStore.applied)); diff --git a/spa/src/views/Home.vue b/spa/src/views/Home.vue index fef42f9..7a2a8c2 100644 --- a/spa/src/views/Home.vue +++ b/spa/src/views/Home.vue @@ -38,6 +38,7 @@ import ftch from "@/utils/ftch.js"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import Banner from "@/components/Banner.vue"; import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; defineOptions({ name: 'Home' @@ -99,7 +100,10 @@ async function onLoadMore() { } onActivated(() => { - yaMetrika.pushHit('/'); + yaMetrika.pushHit('/', { + title: 'Главная страница', + }); + yaMetrika.reachGoal(YA_METRIKA_GOAL.VIEW_HOME); }); onMounted(async () => { diff --git a/spa/src/views/OrderCreated.vue b/spa/src/views/OrderCreated.vue index 0d32781..34682f5 100644 --- a/spa/src/views/OrderCreated.vue +++ b/spa/src/views/OrderCreated.vue @@ -39,10 +39,17 @@ diff --git a/spa/src/views/Product.vue b/spa/src/views/Product.vue index eb964cb..4ebbd8b 100644 --- a/spa/src/views/Product.vue +++ b/spa/src/views/Product.vue @@ -106,7 +106,7 @@ class="btn btn-primary btn-lg w-full" :class="isInCart ? 'btn-success' : 'btn-primary'" :disabled="cart.isLoading || canAddToCart === false" - @click="actionBtnClick" + @click="onCartBtnClick" > {{ btnText }} @@ -156,7 +156,7 @@