Mohammed Foud
commited on
Commit
·
59bee0f
1
Parent(s):
33ed46f
all
Browse files- src/bots/handlers/serviceHandlers.ts +192 -133
- src/bots/utils/keyboardUtils.ts +16 -4
src/bots/handlers/serviceHandlers.ts
CHANGED
|
@@ -49,15 +49,32 @@ export const setupServiceHandlers = (bot: any) => {
|
|
| 49 |
// Add handler for browse services button
|
| 50 |
bot.action('browse_services', async (ctx: BotContext) => {
|
| 51 |
await ctx.answerCbQuery();
|
| 52 |
-
await handleBrowseServices(ctx);
|
| 53 |
});
|
| 54 |
|
| 55 |
// Add handler for services pagination
|
| 56 |
-
bot.action(/^services_page_(\d+)
|
| 57 |
await ctx.answerCbQuery();
|
| 58 |
const match = ctx.match as RegExpMatchArray;
|
| 59 |
const page = parseInt(match[1], 10);
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
});
|
| 62 |
|
| 63 |
// Add handler for sorting countries by price
|
|
@@ -153,7 +170,7 @@ export const setupServiceHandlers = (bot: any) => {
|
|
| 153 |
userSearchStates.delete(telegramId);
|
| 154 |
}
|
| 155 |
try {
|
| 156 |
-
await handleBrowseServices(ctx);
|
| 157 |
} catch (editError: any) {
|
| 158 |
if (editError.message && editError.message.includes('message is not modified')) {
|
| 159 |
logger.info(`Browse services message not modified (cancel_search). No action needed.`);
|
|
@@ -187,157 +204,191 @@ export const setupServiceHandlers = (bot: any) => {
|
|
| 187 |
|
| 188 |
// Text handler for product/country search input
|
| 189 |
bot.on('text', async (ctx: BotContext) => {
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
if (!telegramId || (!userState?.waitingForProductSearch && !userState?.waitingForCountrySearch)) {
|
| 194 |
-
return; // Not in search state, let other text handlers process or ignore
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
// Ensure ctx.message is a text message. Telegraf's `bot.on('text')` guarantees this, but TypeScript needs explicit narrowing.
|
| 198 |
-
const message = ctx.message as Message.TextMessage;
|
| 199 |
-
|
| 200 |
-
// Now 'text' property is safely accessible
|
| 201 |
-
const query = message.text?.toLowerCase().trim();
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
});
|
| 207 |
-
return;
|
| 208 |
-
}
|
| 209 |
|
| 210 |
-
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
const searchResults = Object.entries(fiveSimProducts).filter(([serviceId, serviceData]) => {
|
| 215 |
-
return serviceId.toLowerCase().includes(query) || serviceData.label_en.toLowerCase().includes(query);
|
| 216 |
-
});
|
| 217 |
|
| 218 |
-
if (
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
return;
|
| 223 |
}
|
| 224 |
|
| 225 |
-
|
| 226 |
-
const rowSize = 2; // 2 buttons per row
|
| 227 |
-
const servicesPerPage = 20;
|
| 228 |
-
const currentPage = 0; // Always start search results on the first page
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
)
|
| 244 |
-
);
|
| 245 |
}
|
| 246 |
-
buttons.push(row);
|
| 247 |
-
}
|
| 248 |
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
-
|
| 252 |
-
parse_mode: 'HTML',
|
| 253 |
-
reply_markup: Markup.inlineKeyboard(buttons).reply_markup
|
| 254 |
-
});
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
-
|
| 261 |
-
const
|
| 262 |
-
|
| 263 |
-
});
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
const fullProductPrices = await virtualNumberService.getProductPrices(serviceId);
|
| 269 |
-
if (fullProductPrices && fullProductPrices[serviceId]) {
|
| 270 |
-
productPricesForService = fullProductPrices[serviceId];
|
| 271 |
-
}
|
| 272 |
-
} catch (error: any) {
|
| 273 |
-
logger.error(`Error fetching prices for service ${serviceId} during country search: ${error.message}`);
|
| 274 |
-
await ctx.reply(getServiceErrorMessage(serviceId), {
|
| 275 |
-
parse_mode: 'HTML',
|
| 276 |
-
reply_markup: Markup.inlineKeyboard([Markup.button.callback('🔙 Back to Services', 'browse_services')]).reply_markup
|
| 277 |
});
|
| 278 |
-
return;
|
| 279 |
-
}
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
.
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
});
|
| 301 |
-
return;
|
| 302 |
-
}
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
|
| 314 |
-
|
| 315 |
-
const row = [];
|
| 316 |
-
for (let j = 0; j < rowSize && i + j < pageCountries.length; j++) {
|
| 317 |
-
const { countryId, countryInfo, minPrice, maxPrice } = pageCountries[i + j];
|
| 318 |
-
|
| 319 |
-
const priceText = minPrice === maxPrice
|
| 320 |
-
? `${minPrice}$`
|
| 321 |
-
: `${minPrice}$-${maxPrice}$`;
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
buttons.push(row);
|
| 332 |
}
|
| 333 |
-
}
|
| 334 |
|
| 335 |
-
|
| 336 |
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
}
|
| 342 |
});
|
| 343 |
|
|
@@ -356,7 +407,7 @@ export const setupServiceHandlers = (bot: any) => {
|
|
| 356 |
});
|
| 357 |
};
|
| 358 |
|
| 359 |
-
export const handleBrowseServices = async (ctx: BotContext) => {
|
| 360 |
// 🔒 Authentication check
|
| 361 |
const telegramId = ctx.from?.id;
|
| 362 |
if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
|
|
@@ -377,7 +428,7 @@ export const handleBrowseServices = async (ctx: BotContext) => {
|
|
| 377 |
getServicesPaginationInfo(0),
|
| 378 |
{
|
| 379 |
parse_mode: 'HTML',
|
| 380 |
-
reply_markup: getServicesKeyboard(0).reply_markup
|
| 381 |
}
|
| 382 |
);
|
| 383 |
} catch (editError: any) {
|
|
@@ -389,11 +440,19 @@ export const handleBrowseServices = async (ctx: BotContext) => {
|
|
| 389 |
}
|
| 390 |
};
|
| 391 |
|
| 392 |
-
export const handleServicesPagination = async (ctx: BotContext, page: number) => {
|
| 393 |
// 🔒 Authentication check
|
| 394 |
const telegramId = ctx.from?.id;
|
| 395 |
if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
return; // Explicitly return nothing after editing
|
| 398 |
}
|
| 399 |
|
|
@@ -404,7 +463,7 @@ export const handleServicesPagination = async (ctx: BotContext, page: number) =>
|
|
| 404 |
getServicesPaginationInfo(page),
|
| 405 |
{
|
| 406 |
parse_mode: 'HTML',
|
| 407 |
-
reply_markup: getServicesKeyboard(page).reply_markup
|
| 408 |
}
|
| 409 |
);
|
| 410 |
} catch (editError: any) {
|
|
|
|
| 49 |
// Add handler for browse services button
|
| 50 |
bot.action('browse_services', async (ctx: BotContext) => {
|
| 51 |
await ctx.answerCbQuery();
|
| 52 |
+
await handleBrowseServices(ctx, undefined); // Pass undefined for initial sortBy
|
| 53 |
});
|
| 54 |
|
| 55 |
// Add handler for services pagination
|
| 56 |
+
bot.action(/^services_page_(\d+)(_([a-zA-Z]+))?$/, async (ctx: BotContext) => {
|
| 57 |
await ctx.answerCbQuery();
|
| 58 |
const match = ctx.match as RegExpMatchArray;
|
| 59 |
const page = parseInt(match[1], 10);
|
| 60 |
+
const sortBy = match[3] as 'az' | 'za' | undefined;
|
| 61 |
+
await handleServicesPagination(ctx, page, sortBy);
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
// Add handler for sorting services by A-Z
|
| 65 |
+
bot.action(/^sort_services_az_(\d+)$/, async (ctx: BotContext) => {
|
| 66 |
+
await ctx.answerCbQuery();
|
| 67 |
+
const match = ctx.match as RegExpMatchArray;
|
| 68 |
+
const page = parseInt(match[1], 10);
|
| 69 |
+
await handleServicesPagination(ctx, page, 'az');
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
// Add handler for sorting services by Z-A
|
| 73 |
+
bot.action(/^sort_services_za_(\d+)$/, async (ctx: BotContext) => {
|
| 74 |
+
await ctx.answerCbQuery();
|
| 75 |
+
const match = ctx.match as RegExpMatchArray;
|
| 76 |
+
const page = parseInt(match[1], 10);
|
| 77 |
+
await handleServicesPagination(ctx, page, 'za');
|
| 78 |
});
|
| 79 |
|
| 80 |
// Add handler for sorting countries by price
|
|
|
|
| 170 |
userSearchStates.delete(telegramId);
|
| 171 |
}
|
| 172 |
try {
|
| 173 |
+
await handleBrowseServices(ctx, undefined); // Pass undefined to reset sort
|
| 174 |
} catch (editError: any) {
|
| 175 |
if (editError.message && editError.message.includes('message is not modified')) {
|
| 176 |
logger.info(`Browse services message not modified (cancel_search). No action needed.`);
|
|
|
|
| 204 |
|
| 205 |
// Text handler for product/country search input
|
| 206 |
bot.on('text', async (ctx: BotContext) => {
|
| 207 |
+
try {
|
| 208 |
+
const telegramId = ctx.from?.id;
|
| 209 |
+
const userState = userSearchStates.get(telegramId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
+
if (!telegramId || (!userState?.waitingForProductSearch && !userState?.waitingForCountrySearch)) {
|
| 212 |
+
return; // Not in search state, let other text handlers process or ignore
|
| 213 |
+
}
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
// Ensure ctx.message is a text message. Telegraf's `bot.on('text')` guarantees this, but TypeScript needs explicit narrowing.
|
| 216 |
+
const message = ctx.message as Message.TextMessage;
|
| 217 |
|
| 218 |
+
// Now 'text' property is safely accessible
|
| 219 |
+
const query = message.text?.toLowerCase().trim();
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
+
if (!query || query.length < 2) { // Require at least 2 characters for search
|
| 222 |
+
try {
|
| 223 |
+
await ctx.reply(getInvalidSearchInputMessage(), {
|
| 224 |
+
reply_markup: Markup.inlineKeyboard([Markup.button.callback('↩️ Cancel Search', userState.waitingForProductSearch ? 'cancel_search' : `cancel_country_search_${userState.waitingForCountrySearch?.serviceId}_${userState.waitingForCountrySearch?.sortBy || 'default'}_${userState.waitingForCountrySearch?.page}`)]).reply_markup
|
| 225 |
+
});
|
| 226 |
+
} catch (replyError: any) {
|
| 227 |
+
logger.error(`Error replying with invalid search input message: ${replyError.message}`);
|
| 228 |
+
}
|
| 229 |
return;
|
| 230 |
}
|
| 231 |
|
| 232 |
+
userSearchStates.delete(telegramId); // Clear search state after processing
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
+
if (userState.waitingForProductSearch) {
|
| 235 |
+
// Filter products based on search query
|
| 236 |
+
const searchResults = Object.entries(fiveSimProducts).filter(([serviceId, serviceData]) => {
|
| 237 |
+
return serviceId.toLowerCase().includes(query) || serviceData.label_en.toLowerCase().includes(query);
|
| 238 |
+
});
|
| 239 |
|
| 240 |
+
if (searchResults.length === 0) {
|
| 241 |
+
try {
|
| 242 |
+
await ctx.reply(getNoSearchResultsMessage(), {
|
| 243 |
+
reply_markup: Markup.inlineKeyboard([Markup.button.callback('🔙 Back to Services', 'browse_services')]).reply_markup
|
| 244 |
+
});
|
| 245 |
+
} catch (replyError: any) {
|
| 246 |
+
logger.error(`Error replying with no search results message: ${replyError.message}`);
|
| 247 |
+
}
|
| 248 |
+
return;
|
|
|
|
|
|
|
| 249 |
}
|
|
|
|
|
|
|
| 250 |
|
| 251 |
+
const buttons = [];
|
| 252 |
+
const rowSize = 2; // 2 buttons per row
|
| 253 |
+
const servicesPerPage = 20;
|
| 254 |
+
|
| 255 |
+
const currentPage = 0; // Always start search results on the first page
|
| 256 |
+
|
| 257 |
+
const startIndex = currentPage * servicesPerPage;
|
| 258 |
+
const endIndex = Math.min(startIndex + servicesPerPage, searchResults.length);
|
| 259 |
+
const pageServices = searchResults.slice(startIndex, endIndex);
|
| 260 |
+
|
| 261 |
+
// Generate service buttons in pairs
|
| 262 |
+
for (let i = 0; i < pageServices.length; i += rowSize) {
|
| 263 |
+
const row = [];
|
| 264 |
+
for (let j = 0; j < rowSize && i + j < pageServices.length; j++) {
|
| 265 |
+
const [serviceId, serviceData] = pageServices[i + j];
|
| 266 |
+
row.push(
|
| 267 |
+
Markup.button.callback(
|
| 268 |
+
`${serviceData.icon} ${serviceData.label_en}`,
|
| 269 |
+
`service_${serviceId}`
|
| 270 |
+
)
|
| 271 |
+
);
|
| 272 |
+
}
|
| 273 |
+
buttons.push(row);
|
| 274 |
+
}
|
| 275 |
|
| 276 |
+
buttons.push([Markup.button.callback('🔙 Back to Services', 'browse_services')]);
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
+
try {
|
| 279 |
+
await ctx.reply(getServicesPaginationInfo(currentPage), {
|
| 280 |
+
parse_mode: 'HTML',
|
| 281 |
+
reply_markup: Markup.inlineKeyboard(buttons).reply_markup
|
| 282 |
+
});
|
| 283 |
+
} catch (replyError: any) {
|
| 284 |
+
logger.error(`Error replying with product search results: ${replyError.message}`);
|
| 285 |
+
}
|
| 286 |
|
| 287 |
+
} else if (userState.waitingForCountrySearch) {
|
| 288 |
+
const { serviceId, sortBy, page } = userState.waitingForCountrySearch;
|
| 289 |
+
const allCountries = Object.entries(countryData); // Use all countries for search
|
|
|
|
| 290 |
|
| 291 |
+
const queryResults = allCountries.filter(([countryShortId, countryInfo]) => {
|
| 292 |
+
const searchTarget = `${countryInfo.label.toLowerCase()} ${countryInfo.flag.toLowerCase()} ${countryInfo.code.toLowerCase()} ${countryShortId.toLowerCase()}`;
|
| 293 |
+
return searchTarget.includes(query);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
});
|
|
|
|
|
|
|
| 295 |
|
| 296 |
+
// Get prices for the selected service to filter countries by availability and get min/max prices
|
| 297 |
+
let productPricesForService: any = {};
|
| 298 |
+
try {
|
| 299 |
+
const fullProductPrices = await virtualNumberService.getProductPrices(serviceId);
|
| 300 |
+
if (fullProductPrices && fullProductPrices[serviceId]) {
|
| 301 |
+
productPricesForService = fullProductPrices[serviceId];
|
| 302 |
+
}
|
| 303 |
+
} catch (error: any) {
|
| 304 |
+
logger.error(`Error fetching prices for service ${serviceId} during country search: ${error.message}`);
|
| 305 |
+
try {
|
| 306 |
+
await ctx.reply(getServiceErrorMessage(serviceId), {
|
| 307 |
+
parse_mode: 'HTML',
|
| 308 |
+
reply_markup: Markup.inlineKeyboard([Markup.button.callback('🔙 Back to Services', 'browse_services')]).reply_markup
|
| 309 |
+
});
|
| 310 |
+
} catch (replyError: any) {
|
| 311 |
+
logger.error(`Error replying with service error message during country search: ${replyError.message}`);
|
| 312 |
+
}
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
|
| 316 |
+
const searchResults = queryResults.filter(([countryShortId, _]) => {
|
| 317 |
+
const countryOperators = productPricesForService[countryShortId];
|
| 318 |
+
// Only include countries that have available numbers for the current service
|
| 319 |
+
return countryOperators && Object.values(countryOperators).some((op: any) => op.count > 0);
|
| 320 |
+
}).map(([countryId, countryInfo]) => {
|
| 321 |
+
const operators = productPricesForService[countryId];
|
| 322 |
+
const prices = Object.values(operators)
|
| 323 |
+
.filter((op: any) => op.count > 0)
|
| 324 |
+
.map((op: any) => calculatePriceWithProfit(ctx, op.cost, 'RUB'));
|
| 325 |
+
|
| 326 |
+
const minPrice = prices.length > 0 ? parseFloat(Math.min(...prices).toFixed(2)) : 0;
|
| 327 |
+
const maxPrice = prices.length > 0 ? parseFloat(Math.max(...prices).toFixed(2)) : 0;
|
| 328 |
+
|
| 329 |
+
return { countryId, countryInfo, minPrice, maxPrice };
|
| 330 |
});
|
|
|
|
|
|
|
| 331 |
|
| 332 |
+
if (searchResults.length === 0) {
|
| 333 |
+
try {
|
| 334 |
+
await ctx.reply(getNoCountrySearchResultsMessage(), {
|
| 335 |
+
reply_markup: Markup.inlineKeyboard([Markup.button.callback('↩️ Cancel Search', `cancel_country_search_${serviceId}_${sortBy || 'default'}_${page}`)]).reply_markup
|
| 336 |
+
});
|
| 337 |
+
} catch (replyError: any) {
|
| 338 |
+
logger.error(`Error replying with no country search results message: ${replyError.message}`);
|
| 339 |
+
}
|
| 340 |
+
return;
|
| 341 |
+
}
|
| 342 |
|
| 343 |
+
const buttons = [];
|
| 344 |
+
const rowSize = 2;
|
| 345 |
+
const countriesPerPage = 10;
|
| 346 |
|
| 347 |
+
const currentPage = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
+
const startIndex = currentPage * countriesPerPage;
|
| 350 |
+
const endIndex = Math.min(startIndex + countriesPerPage, searchResults.length);
|
| 351 |
+
const pageCountries = searchResults.slice(startIndex, endIndex);
|
| 352 |
+
|
| 353 |
+
for (let i = 0; i < pageCountries.length; i += rowSize) {
|
| 354 |
+
const row = [];
|
| 355 |
+
for (let j = 0; j < rowSize && i + j < pageCountries.length; j++) {
|
| 356 |
+
const { countryId, countryInfo, minPrice, maxPrice } = pageCountries[i + j];
|
| 357 |
+
|
| 358 |
+
const priceText = minPrice === maxPrice
|
| 359 |
+
? `${minPrice}$`
|
| 360 |
+
: `${minPrice}$-${maxPrice}$`;
|
| 361 |
+
|
| 362 |
+
row.push(
|
| 363 |
+
Markup.button.callback(
|
| 364 |
+
`${countryInfo.flag} ${countryInfo.label} (${priceText})`,
|
| 365 |
+
`country_${serviceId}_${countryId}`
|
| 366 |
+
)
|
| 367 |
+
);
|
| 368 |
+
}
|
| 369 |
buttons.push(row);
|
| 370 |
}
|
|
|
|
| 371 |
|
| 372 |
+
buttons.push([Markup.button.callback('↩️ Back to Countries', `cancel_country_search_${serviceId}_${sortBy || 'default'}_${page}`)]);
|
| 373 |
|
| 374 |
+
try {
|
| 375 |
+
await ctx.reply(getCountriesListMessage(serviceId), {
|
| 376 |
+
parse_mode: 'HTML',
|
| 377 |
+
reply_markup: Markup.inlineKeyboard(buttons).reply_markup
|
| 378 |
+
});
|
| 379 |
+
} catch (replyError: any) {
|
| 380 |
+
logger.error(`Error replying with country search results: ${replyError.message}`);
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
} catch (handlerError: any) {
|
| 384 |
+
logger.error(`Unhandled error in text handler: ${handlerError.message}`);
|
| 385 |
+
try {
|
| 386 |
+
await ctx.reply('⚠️ An unexpected error occurred. Please try again or go back to the main menu.', {
|
| 387 |
+
reply_markup: getLoggedInMenuKeyboard().reply_markup
|
| 388 |
+
});
|
| 389 |
+
} catch (finalReplyError: any) {
|
| 390 |
+
logger.error(`Error sending final error message: ${finalReplyError.message}`);
|
| 391 |
+
}
|
| 392 |
}
|
| 393 |
});
|
| 394 |
|
|
|
|
| 407 |
});
|
| 408 |
};
|
| 409 |
|
| 410 |
+
export const handleBrowseServices = async (ctx: BotContext, sortBy?: 'az' | 'za') => {
|
| 411 |
// 🔒 Authentication check
|
| 412 |
const telegramId = ctx.from?.id;
|
| 413 |
if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
|
|
|
|
| 428 |
getServicesPaginationInfo(0),
|
| 429 |
{
|
| 430 |
parse_mode: 'HTML',
|
| 431 |
+
reply_markup: getServicesKeyboard(0, sortBy).reply_markup
|
| 432 |
}
|
| 433 |
);
|
| 434 |
} catch (editError: any) {
|
|
|
|
| 440 |
}
|
| 441 |
};
|
| 442 |
|
| 443 |
+
export const handleServicesPagination = async (ctx: BotContext, page: number, sortBy?: 'az' | 'za') => {
|
| 444 |
// 🔒 Authentication check
|
| 445 |
const telegramId = ctx.from?.id;
|
| 446 |
if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
|
| 447 |
+
try {
|
| 448 |
+
await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
|
| 449 |
+
} catch (editError: any) {
|
| 450 |
+
if (editError.message && editError.message.includes('message is not modified')) {
|
| 451 |
+
logger.info(`Auth required message not modified. No action needed.`);
|
| 452 |
+
} else {
|
| 453 |
+
logger.error(`Error editing auth required message: ${editError.message}`);
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
return; // Explicitly return nothing after editing
|
| 457 |
}
|
| 458 |
|
|
|
|
| 463 |
getServicesPaginationInfo(page),
|
| 464 |
{
|
| 465 |
parse_mode: 'HTML',
|
| 466 |
+
reply_markup: getServicesKeyboard(page, sortBy).reply_markup // Pass sortBy here
|
| 467 |
}
|
| 468 |
);
|
| 469 |
} catch (editError: any) {
|
src/bots/utils/keyboardUtils.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const getLoggedInMenuKeyboard = () => {
|
|
| 36 |
]);
|
| 37 |
};
|
| 38 |
|
| 39 |
-
export const getServicesKeyboard = (page: number = 0) => {
|
| 40 |
const buttons = [];
|
| 41 |
const services = Object.entries(fiveSimProducts);
|
| 42 |
const rowSize = 2; // 2 buttons per row
|
|
@@ -46,7 +46,19 @@ export const getServicesKeyboard = (page: number = 0) => {
|
|
| 46 |
// Get services for current page
|
| 47 |
const startIndex = page * servicesPerPage;
|
| 48 |
const endIndex = Math.min(startIndex + servicesPerPage, services.length);
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
// Generate service buttons in pairs
|
| 52 |
for (let i = 0; i < pageServices.length; i += rowSize) {
|
|
@@ -69,7 +81,7 @@ export const getServicesKeyboard = (page: number = 0) => {
|
|
| 69 |
paginationRow.push(
|
| 70 |
Markup.button.callback(
|
| 71 |
'⬅️ Previous',
|
| 72 |
-
`services_page_${page - 1}`
|
| 73 |
)
|
| 74 |
);
|
| 75 |
}
|
|
@@ -85,7 +97,7 @@ export const getServicesKeyboard = (page: number = 0) => {
|
|
| 85 |
paginationRow.push(
|
| 86 |
Markup.button.callback(
|
| 87 |
'Next ➡️',
|
| 88 |
-
`services_page_${page + 1}`
|
| 89 |
)
|
| 90 |
);
|
| 91 |
}
|
|
|
|
| 36 |
]);
|
| 37 |
};
|
| 38 |
|
| 39 |
+
export const getServicesKeyboard = (page: number = 0, sortBy?: 'az' | 'za') => {
|
| 40 |
const buttons = [];
|
| 41 |
const services = Object.entries(fiveSimProducts);
|
| 42 |
const rowSize = 2; // 2 buttons per row
|
|
|
|
| 46 |
// Get services for current page
|
| 47 |
const startIndex = page * servicesPerPage;
|
| 48 |
const endIndex = Math.min(startIndex + servicesPerPage, services.length);
|
| 49 |
+
let pageServices = services.slice(startIndex, endIndex);
|
| 50 |
+
|
| 51 |
+
// Apply sorting for services
|
| 52 |
+
if (sortBy === 'az') {
|
| 53 |
+
pageServices.sort((a, b) => a[1].label_en.localeCompare(b[1].label_en));
|
| 54 |
+
} else if (sortBy === 'za') {
|
| 55 |
+
pageServices.sort((a, b) => b[1].label_en.localeCompare(a[1].label_en));
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Add sort button
|
| 59 |
+
const sortButtonText = sortBy === 'az' ? 'Sort Z-A ⬇️' : 'Sort A-Z ⬆️';
|
| 60 |
+
const sortButtonCallback = sortBy === 'az' ? `sort_services_za_${page}` : `sort_services_az_${page}`;
|
| 61 |
+
buttons.push([Markup.button.callback(sortButtonText, sortButtonCallback)]);
|
| 62 |
|
| 63 |
// Generate service buttons in pairs
|
| 64 |
for (let i = 0; i < pageServices.length; i += rowSize) {
|
|
|
|
| 81 |
paginationRow.push(
|
| 82 |
Markup.button.callback(
|
| 83 |
'⬅️ Previous',
|
| 84 |
+
`services_page_${page - 1}${sortBy ? `_${sortBy}` : ''}`
|
| 85 |
)
|
| 86 |
);
|
| 87 |
}
|
|
|
|
| 97 |
paginationRow.push(
|
| 98 |
Markup.button.callback(
|
| 99 |
'Next ➡️',
|
| 100 |
+
`services_page_${page + 1}${sortBy ? `_${sortBy}` : ''}`
|
| 101 |
)
|
| 102 |
);
|
| 103 |
}
|