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:
@@ -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 {
|
||||||
|
|
||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)}` }]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user