fix: location navigation uses IDs and pipe separators instead of underscore

Critical fix for product management location selection:
- Country/city callback_data now uses pipe | as separator with
  encodeURIComponent/decodeURIComponent for special chars
- District selection uses location ID (prod_loc_{id}, shop_loc_{id})
  instead of underscore-delimited country_city_district text
- Empty district names now show city name as fallback
- LocationService.getLocationsByCountryAndCity() returns id+district
  for building callback_data with location IDs
- All error handlers in admin product navigation use editOrSendCallback
  to avoid chat clutter
- Routes updated: prod_district_ → prod_loc_, shop_district_ → shop_loc_

This fixes the bug where selecting country/city/district in admin panel
or shop failed because split('_') broke on multi-word names or empty
district values.
This commit is contained in:
NW
2026-06-24 22:44:02 +01:00
parent 6ce8da257a
commit 5a9155613e
6 changed files with 56 additions and 47 deletions

View File

@@ -4,6 +4,7 @@ import bot from '../../../context/bot.js';
import CategoryService from '../../../services/categoryService.js'; import CategoryService from '../../../services/categoryService.js';
import ProductService from '../../../services/productService.js'; import ProductService from '../../../services/productService.js';
import logger from '../../../utils/logger.js'; import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class CategorySelectionHandler { export default class CategorySelectionHandler {
@@ -19,9 +20,9 @@ export default class CategorySelectionHandler {
try { try {
const category = await CategoryService.getCategoryById(categoryId); const category = await CategoryService.getCategoryById(categoryId);
const location = await LocationService.getLocationById(locationId); const location = await LocationService.getLocationById(locationId);
const products = await ProductService.getProductsByCategoryId(categoryId); const products = await ProductService.getProductsByCategoryId(categoryId);
const keyboard = { const keyboard = {
inline_keyboard: [ inline_keyboard: [
...products.map(prod => [{ ...products.map(prod => [{
@@ -29,7 +30,7 @@ export default class CategorySelectionHandler {
callback_data: `view_product_${prod.id}` callback_data: `view_product_${prod.id}`
}]), }]),
[{ text: ' Add Product', callback_data: `add_product_${locationId}_${categoryId}` }], [{ text: ' Add Product', callback_data: `add_product_${locationId}_${categoryId}` }],
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] [{ text: '« Back', callback_data: `prod_loc_${locationId}` }]
] ]
}; };
@@ -43,7 +44,7 @@ export default class CategorySelectionHandler {
); );
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleCategorySelection'); logger.error({ err: error }, 'Error in handleCategorySelection');
await bot.sendMessage(chatId, 'Error loading products. Please try again.'); await editOrSendCallback(callbackQuery, 'Error loading products. Please try again.');
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import CategoryService from '../../../services/categoryService.js';
import bot from '../../../context/bot.js'; import bot from '../../../context/bot.js';
import userStates from '../../../context/userStates.js'; import userStates from '../../../context/userStates.js';
import logger from '../../../utils/logger.js'; import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class DistrictHandler { export default class DistrictHandler {
@@ -14,18 +15,19 @@ export default class DistrictHandler {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('prod_city_', '').split('_'); const payload = callbackQuery.data.replace('prod_city_', '');
const [country, city] = payload.split('|').map(decodeURIComponent);
try { try {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city); const locations = await LocationService.getLocationsByCountryAndCity(country, city);
const keyboard = { const keyboard = {
inline_keyboard: [ inline_keyboard: [
...districts.map(loc => [{ ...locations.map(loc => [{
text: loc.district, text: loc.district || loc.city,
callback_data: `prod_district_${country}_${city}_${loc.district}` callback_data: `prod_loc_${loc.id}`
}]), }]),
[{text: '« Back', callback_data: `prod_country_${country}`}] [{text: '« Back', callback_data: `prod_country_${encodeURIComponent(country)}`}]
] ]
}; };
@@ -39,7 +41,7 @@ export default class DistrictHandler {
); );
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleCitySelection'); logger.error({ err: error }, 'Error in handleCitySelection');
await bot.sendMessage(chatId, 'Error loading districts. Please try again.'); await editOrSendCallback(callbackQuery, 'Error loading districts. Please try again.');
} }
} }
@@ -50,12 +52,12 @@ export default class DistrictHandler {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_'); const locationId = parseInt(callbackQuery.data.replace('prod_loc_', ''), 10);
await userStates.delete(chatId); await userStates.delete(chatId);
try { try {
const location = await LocationService.getLocation(country, city, district); const location = await LocationService.getLocationById(locationId);
if (!location) { if (!location) {
throw new Error('Location not found'); throw new Error('Location not found');
@@ -70,7 +72,7 @@ export default class DistrictHandler {
callback_data: `prod_category_${location.id}_${cat.id}` callback_data: `prod_category_${location.id}_${cat.id}`
}]), }]),
[{text: ' Add Category', callback_data: `add_category_${location.id}`}], [{text: ' Add Category', callback_data: `add_category_${location.id}`}],
[{text: '« Back', callback_data: `prod_city_${country}_${city}`}] [{text: '« Back', callback_data: `prod_city_${encodeURIComponent(location.country)}|${encodeURIComponent(location.city)}`}]
] ]
}; };
@@ -84,7 +86,7 @@ export default class DistrictHandler {
); );
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleDistrictSelection'); logger.error({ err: error }, 'Error in handleDistrictSelection');
await bot.sendMessage(chatId, 'Error loading categories. Please try again.'); await editOrSendCallback(callbackQuery, 'Error loading categories. Please try again.');
} }
} }
} }

View File

@@ -2,6 +2,7 @@ import { isAdmin } from '../../../middleware/auth.js';
import LocationService from '../../../services/locationService.js'; import LocationService from '../../../services/locationService.js';
import bot from '../../../context/bot.js'; import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js'; import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class NavigationHandler { export default class NavigationHandler {
@@ -14,7 +15,7 @@ export default class NavigationHandler {
} }
try { try {
const countries = await LocationService.getCountries() const countries = await LocationService.getCountries();
if (countries.length === 0) { if (countries.length === 0) {
await bot.sendMessage( await bot.sendMessage(
@@ -34,7 +35,7 @@ export default class NavigationHandler {
const keyboard = { const keyboard = {
inline_keyboard: countries.map(loc => [{ inline_keyboard: countries.map(loc => [{
text: loc.country, text: loc.country,
callback_data: `prod_country_${loc.country}` callback_data: `prod_country_${encodeURIComponent(loc.country)}`
}]) }])
}; };
@@ -56,16 +57,16 @@ export default class NavigationHandler {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('prod_country_', ''); const country = decodeURIComponent(callbackQuery.data.replace('prod_country_', ''));
try { try {
const cities = await LocationService.getCitiesByCountry(country) const cities = await LocationService.getCitiesByCountry(country);
const keyboard = { const keyboard = {
inline_keyboard: [ inline_keyboard: [
...cities.map(loc => [{ ...cities.map(loc => [{
text: loc.city, text: loc.city,
callback_data: `prod_city_${country}_${loc.city}` callback_data: `prod_city_${encodeURIComponent(country)}|${encodeURIComponent(loc.city)}`
}]), }]),
[{text: '« Back', callback_data: 'manage_products'}] [{text: '« Back', callback_data: 'manage_products'}]
] ]
@@ -81,7 +82,7 @@ export default class NavigationHandler {
); );
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleCountrySelection'); logger.error({ err: error }, 'Error in handleCountrySelection');
await bot.sendMessage(chatId, 'Error loading cities. Please try again.'); await editOrSendCallback(callbackQuery, 'Error loading cities. Please try again.');
} }
} }
} }

View File

@@ -62,7 +62,7 @@ export default class UserProductHandler {
const keyboard = { const keyboard = {
inline_keyboard: countries.map(loc => [{ inline_keyboard: countries.map(loc => [{
text: loc.country, text: loc.country,
callback_data: `shop_country_${loc.country}` callback_data: `shop_country_${encodeURIComponent(loc.country)}`
}]) }])
}; };
@@ -88,7 +88,7 @@ export default class UserProductHandler {
static async handleCountrySelection(callbackQuery) { static async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('shop_country_', ''); const country = decodeURIComponent(callbackQuery.data.replace('shop_country_', ''));
try { try {
const cities = await LocationService.getCitiesByCountry(country); const cities = await LocationService.getCitiesByCountry(country);
@@ -97,7 +97,7 @@ export default class UserProductHandler {
inline_keyboard: [ inline_keyboard: [
...cities.map(loc => [{ ...cities.map(loc => [{
text: loc.city, text: loc.city,
callback_data: `shop_city_${country}_${loc.city}` callback_data: `shop_city_${encodeURIComponent(country)}|${encodeURIComponent(loc.city)}`
}]), }]),
[{text: '« Back to Countries', callback_data: 'shop_start'}] [{text: '« Back to Countries', callback_data: 'shop_start'}]
] ]
@@ -113,25 +113,26 @@ export default class UserProductHandler {
); );
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleCountrySelection'); logger.error({ err: error }, 'Error in handleCountrySelection');
await bot.sendMessage(chatId, 'Error loading cities. Please try again.'); await editOrSendCallback(callbackQuery, 'Error loading cities. Please try again.');
} }
} }
static async handleCitySelection(callbackQuery) { static async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('shop_city_', '').split('_'); const payload = callbackQuery.data.replace('shop_city_', '');
const [country, city] = payload.split('|').map(decodeURIComponent);
try { try {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city) const locations = await LocationService.getLocationsByCountryAndCity(country, city);
const keyboard = { const keyboard = {
inline_keyboard: [ inline_keyboard: [
...districts.map(loc => [{ ...locations.map(loc => [{
text: loc.district, text: loc.district || loc.city,
callback_data: `shop_district_${country}_${city}_${loc.district}` callback_data: `shop_loc_${loc.id}`
}]), }]),
[{text: '« Back to Cities', callback_data: `shop_country_${country}`}] [{text: '« Back to Cities', callback_data: `shop_country_${encodeURIComponent(country)}`}]
] ]
}; };
@@ -145,21 +146,19 @@ export default class UserProductHandler {
); );
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleCitySelection'); logger.error({ err: error }, 'Error in handleCitySelection');
await bot.sendMessage(chatId, 'Error loading districts. Please try again.'); await editOrSendCallback(callbackQuery, 'Error loading districts. Please try again.');
} }
} }
static async handleDistrictSelection(callbackQuery) { static async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_'); const locationId = parseInt(callbackQuery.data.replace('shop_loc_', ''), 10);
try { try {
// Получаем информацию о локации const location = await LocationService.getLocationById(locationId);
const location = await LocationService.getLocation(country, city, district);
if (!location) { if (!location) {
// Если локация не найдена, вернуть пользователя к предыдущему шагу
await bot.editMessageText( await bot.editMessageText(
'Location not found. Returning to previous menu.', 'Location not found. Returning to previous menu.',
{ {
@@ -167,7 +166,7 @@ export default class UserProductHandler {
message_id: messageId, message_id: messageId,
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '« Back', callback_data: `shop_city_${country}_${city}` } { text: '« Back', callback_data: `shop_city_${encodeURIComponent(location?.country || '')}|${encodeURIComponent(location?.city || '')}` }
]] ]]
} }
} }
@@ -175,12 +174,11 @@ export default class UserProductHandler {
return; return;
} }
// Сохраняем текстовое представление локации в состоянии пользователя
await userStates.set(chatId, { await userStates.set(chatId, {
location: `${country}_${city}_${district}` location: `${location.country}_${location.city}_${location.district}`,
locationId: location.id
}); });
// Получаем категории для выбранной локации
const categories = await CategoryService.getCategoriesByLocationId(location.id); const categories = await CategoryService.getCategoriesByLocationId(location.id);
const keyboard = { const keyboard = {
@@ -189,7 +187,7 @@ export default class UserProductHandler {
text: cat.name, text: cat.name,
callback_data: `shop_category_${location.id}_${cat.id}` callback_data: `shop_category_${location.id}_${cat.id}`
}]), }]),
[{ text: '« Back', callback_data: `shop_city_${country}_${city}` }] [{ text: '« Back', callback_data: `shop_city_${encodeURIComponent(location.country)}|${encodeURIComponent(location.city)}` }]
] ]
}; };

View File

@@ -181,7 +181,7 @@ export function registerRoutes() {
logDebug(cq.data, 'handleCitySelection'); logDebug(cq.data, 'handleCitySelection');
await userProductHandler.handleCitySelection(cq); await userProductHandler.handleCitySelection(cq);
}); });
callbackRouter.registerPrefix('shop_district_', async (cq) => { callbackRouter.registerPrefix('shop_loc_', async (cq) => {
logDebug(cq.data, 'handleDistrictSelection'); logDebug(cq.data, 'handleDistrictSelection');
await userProductHandler.handleDistrictSelection(cq); await userProductHandler.handleDistrictSelection(cq);
}); });
@@ -233,7 +233,7 @@ export function registerRoutes() {
logDebug(cq.data, 'handleCitySelection'); logDebug(cq.data, 'handleCitySelection');
await productHandler.handleCitySelection(cq); await productHandler.handleCitySelection(cq);
}); });
callbackRouter.registerPrefix('prod_district_', async (cq) => { callbackRouter.registerPrefix('prod_loc_', async (cq) => {
logDebug(cq.data, 'handleDistrictSelection'); logDebug(cq.data, 'handleDistrictSelection');
await productHandler.handleDistrictSelection(cq); await productHandler.handleDistrictSelection(cq);
}); });

View File

@@ -15,7 +15,14 @@ class LocationService {
static async getDistrictsByCountryAndCity(country, city) { static async getDistrictsByCountryAndCity(country, city) {
return await db.allAsync( return await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district', 'SELECT id, district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
}
static async getLocationsByCountryAndCity(country, city) {
return await db.allAsync(
'SELECT id, country, city, district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city] [country, city]
); );
} }