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 ProductService from '../../../services/productService.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class CategorySelectionHandler {
@@ -19,9 +20,9 @@ export default class CategorySelectionHandler {
try {
const category = await CategoryService.getCategoryById(categoryId);
const location = await LocationService.getLocationById(locationId);
const products = await ProductService.getProductsByCategoryId(categoryId);
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
@@ -29,7 +30,7 @@ export default class CategorySelectionHandler {
callback_data: `view_product_${prod.id}`
}]),
[{ 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) {
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 userStates from '../../../context/userStates.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class DistrictHandler {
@@ -14,18 +15,19 @@ export default class DistrictHandler {
const chatId = callbackQuery.message.chat.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 {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city);
const locations = await LocationService.getLocationsByCountryAndCity(country, city);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `prod_district_${country}_${city}_${loc.district}`
...locations.map(loc => [{
text: loc.district || loc.city,
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) {
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 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);
try {
const location = await LocationService.getLocation(country, city, district);
const location = await LocationService.getLocationById(locationId);
if (!location) {
throw new Error('Location not found');
@@ -70,7 +72,7 @@ export default class DistrictHandler {
callback_data: `prod_category_${location.id}_${cat.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) {
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 bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
export default class NavigationHandler {
@@ -14,7 +15,7 @@ export default class NavigationHandler {
}
try {
const countries = await LocationService.getCountries()
const countries = await LocationService.getCountries();
if (countries.length === 0) {
await bot.sendMessage(
@@ -34,7 +35,7 @@ export default class NavigationHandler {
const keyboard = {
inline_keyboard: countries.map(loc => [{
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 messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('prod_country_', '');
const country = decodeURIComponent(callbackQuery.data.replace('prod_country_', ''));
try {
const cities = await LocationService.getCitiesByCountry(country)
const cities = await LocationService.getCitiesByCountry(country);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
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'}]
]
@@ -81,7 +82,7 @@ export default class NavigationHandler {
);
} catch (error) {
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 = {
inline_keyboard: countries.map(loc => [{
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) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('shop_country_', '');
const country = decodeURIComponent(callbackQuery.data.replace('shop_country_', ''));
try {
const cities = await LocationService.getCitiesByCountry(country);
@@ -97,7 +97,7 @@ export default class UserProductHandler {
inline_keyboard: [
...cities.map(loc => [{
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'}]
]
@@ -113,25 +113,26 @@ export default class UserProductHandler {
);
} catch (error) {
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) {
const chatId = callbackQuery.message.chat.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 {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city)
const locations = await LocationService.getLocationsByCountryAndCity(country, city);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `shop_district_${country}_${city}_${loc.district}`
...locations.map(loc => [{
text: loc.district || loc.city,
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) {
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) {
const chatId = callbackQuery.message.chat.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 {
// Получаем информацию о локации
const location = await LocationService.getLocation(country, city, district);
const location = await LocationService.getLocationById(locationId);
if (!location) {
// Если локация не найдена, вернуть пользователя к предыдущему шагу
await bot.editMessageText(
'Location not found. Returning to previous menu.',
{
@@ -167,7 +166,7 @@ export default class UserProductHandler {
message_id: messageId,
reply_markup: {
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;
}
// Сохраняем текстовое представление локации в состоянии пользователя
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 keyboard = {
@@ -189,7 +187,7 @@ export default class UserProductHandler {
text: cat.name,
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');
await userProductHandler.handleCitySelection(cq);
});
callbackRouter.registerPrefix('shop_district_', async (cq) => {
callbackRouter.registerPrefix('shop_loc_', async (cq) => {
logDebug(cq.data, 'handleDistrictSelection');
await userProductHandler.handleDistrictSelection(cq);
});
@@ -233,7 +233,7 @@ export function registerRoutes() {
logDebug(cq.data, 'handleCitySelection');
await productHandler.handleCitySelection(cq);
});
callbackRouter.registerPrefix('prod_district_', async (cq) => {
callbackRouter.registerPrefix('prod_loc_', async (cq) => {
logDebug(cq.data, 'handleDistrictSelection');
await productHandler.handleDistrictSelection(cq);
});

View File

@@ -15,7 +15,14 @@ class LocationService {
static async getDistrictsByCountryAndCity(country, city) {
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]
);
}