const { Markup } = require('telegraf'); const Product = require('../models/Product'); const Category = require('../models/Category'); const Order = require('../models/Order'); const User = require('../models/User'); // Fixed Missing Import const config = require('../config'); // Main Menu exports.start = (ctx, text) => { const userId = ctx.from.id.toString(); const isAdmin = config.ADMIN_IDS.includes(userId); // Ensure i18n is available (fallback if direct call) const i18n = ctx.i18n || require('../locales').uz; // Ensure proper buttons with fallbacks const btn = (key, fallback) => key || fallback; let buttons = []; if (isAdmin) { buttons = [ [btn(i18n.btn_catalog, "πŸ› Katalog"), btn(i18n.btn_search, "πŸ”Ž Qidiruv")], [btn(i18n.btn_orders, "πŸ“¦ Buyurtmalarim"), btn(i18n.btn_cart, "πŸ›’ Savat")], ["πŸ‘¨β€πŸ’Ό Admin Panel", btn(i18n.btn_contact, "πŸ“ž Aloqa")], // JOINED [btn(i18n.btn_channel, "πŸ“’ Kanal"), btn(i18n.btn_group, "πŸ‘₯ Guruh")], [btn(i18n.btn_invite, "πŸ‘₯ Taklif"), btn(i18n.btn_lang, "🌍 Til")] ]; } else { buttons = [ [btn(i18n.btn_catalog, "πŸ› Katalog"), btn(i18n.btn_search, "πŸ”Ž Qidiruv")], [btn(i18n.btn_orders, "πŸ“¦ Buyurtmalarim"), btn(i18n.btn_cart, "πŸ›’ Savat")], [btn(i18n.btn_channel, "πŸ“’ Kanal"), btn(i18n.btn_group, "πŸ‘₯ Guruh")], [btn(i18n.btn_contact, "πŸ“ž Aloqa"), btn(i18n.btn_lang, "🌍 Til")], [btn(i18n.btn_invite, "πŸ‘₯ Taklif")] ]; } // Filter out any undefined or empty strings just in case buttons = buttons.map(row => row.filter(b => b)); const msg = text || i18n.menu_main || "Menu"; return ctx.replyWithHTML(msg, Markup.keyboard(buttons).resize()); }; // Handle Referral Payload specifically (Called from bot.js) exports.handleReferral = async (ctx, referrerId) => { if (!referrerId) return; // Prevent self-referral if (referrerId === ctx.from.id.toString()) return; const user = await User.findOne({ id: ctx.from.id }); if (user && !user.referredBy) { // Only if user exists and hasn't been referred yet? // Actually, usually referrals are for NEW users. // If user is already in DB, maybe we don't count it? // But "full logic" implies we adhere to standard rules: New users only. // Let's assume we tracked it when creating the user. // But the user creation happens in middleware or bot.js start handler. // Let's update it here. user.referredBy = referrerId; await user.save(); // Notify Referrer try { // Need to get referrer language, but might be expensive. Assume Uzbek or use simple text? // "Maximal Logic" implies we should respect referrer's language if possible. // But we don't have referrer's context (ctx) here, only ID. // We can fetch referrer user. const referrerUser = await User.findOne({ id: referrerId }); if (referrerUser) { // Increment Count referrerUser.referralCount = (referrerUser.referralCount || 0) + 1; await referrerUser.save(); const locales = require('../locales'); const rLang = locales[referrerUser.language] || locales.uz; await ctx.telegram.sendMessage(referrerId, `${rLang.invite_referral} ${ctx.from.first_name}!\n${rLang.invite_stats.replace('{count}', referrerUser.referralCount)}`); } } catch (e) { // Referrer might have blocked bot } } }; // Invite Friends Feature exports.inviteFriends = async (ctx) => { const botUsername = ctx.botInfo.username; const inviteLink = `https://t.me/${botUsername}?start=${ctx.from.id}`; // Get Stats const user = await User.findOne({ id: ctx.from.id }); const count = user ? (user.referralCount || 0) : 0; const statsText = ctx.i18n.invite_stats.replace('{count}', count); const text = `${ctx.i18n.invite_text}\n\n${statsText}\n\nπŸ”— ${inviteLink}\n\nπŸ‘‡`; const forwardText = `${ctx.i18n.invite_forward}${inviteLink}`; await ctx.replyWithHTML(text); await ctx.replyWithHTML(forwardText); }; // Settings are already localized in previous steps exports.changeName = async (ctx) => { ctx.reply(ctx.i18n.name_change_prompt, Markup.keyboard([[ctx.i18n.btn_cancel]]).resize()); ctx.session.isChangingName = true; }; exports.handleNameChange = async (ctx) => { if (!ctx.session.isChangingName) return false; const newName = ctx.message.text; // Check for cancel using localization keys manually (or just hardcode known cancels) // Better to use universal check if possible, or just standard "Cancel" text from i18n if (newName === ctx.i18n.btn_cancel || newName === '❌ Bekor qilish' || newName === '❌ ΠžΡ‚ΠΌΠ΅Π½Π°' || newName === '❌ Cancel') { ctx.session.isChangingName = false; exports.start(ctx, ctx.i18n.name_change_cancel); return true; } await User.updateOne({ id: ctx.from.id }, { first_name: newName }); ctx.session.isChangingName = false; ctx.reply(`${ctx.i18n.name_change_success} ${newName}`); exports.start(ctx); // Return to menu return true; }; // Show Categories (Root: parent is null) exports.showCategories = async (ctx) => { try { const categories = await Category.find({ parent: null }); // Only Root if (categories.length === 0) return ctx.reply(ctx.i18n.no_cats); const buttons = []; for (const c of categories) { // Find all subcategories to count correctly const subCats = await Category.find({ parent: c.id }); const subCatNames = subCats.map(sub => sub.name); const targetCategories = [...subCatNames, c.name]; const countNew = await Product.countDocuments({ category: { $in: targetCategories }, $or: [{ condition: 'new' }, { condition: { $exists: false } }] }); const countUsed = await Product.countDocuments({ category: { $in: targetCategories }, condition: 'used' }); // If empty, show nothing? Or generic? Let's show if > 0 if (countNew > 0) { buttons.push([Markup.button.callback(`πŸ†• ${c.name} Yangi (${countNew})`, `cat_${c.id}_new`)]); } if (countUsed > 0) { buttons.push([Markup.button.callback(`♻️ ${c.name} B/U (${countUsed})`, `cat_${c.id}_used`)]); } // Fallback if truly empty but category exists (show generic to avoid hidden cats) if (countNew === 0 && countUsed === 0) { buttons.push([Markup.button.callback(`${c.name} (0)`, `cat_${c.id}_all`)]); } } buttons.push([Markup.button.callback(ctx.i18n.btn_back || "πŸ”™ Orqaga", "back_to_menu")]); await ctx.reply(ctx.i18n.cat_select, Markup.inlineKeyboard(buttons)); } catch (err) { console.error(err); ctx.reply("Error"); } }; // Show Subcategories exports.showSubCategories = async (ctx, parentId, inheritedCondition = null) => { try { const parent = await Category.findOne({ id: parentId }); const subCats = await Category.find({ parent: parentId }); if (!subCats || subCats.length === 0) { // Fallback to products if no subcategories // Pass the condition if we have one return exports.browseCategory(ctx, `cat_${parentId}`, 0, 0, inheritedCondition || 'all'); } const buttons = []; for (const c of subCats) { // Count products in this subcategory // Use similar logic: filter by condition if inherited, or show split if not? // User request: "har bita kataloga alohida... sorasin" // If I selected "New" at root, I should probably only see "New" subcategories or items. let queryNew = { category: c.name, $or: [{ condition: 'new' }, { condition: { $exists: false } }] }; let queryUsed = { category: c.name, condition: 'used' }; const countNew = await Product.countDocuments(queryNew); const countUsed = await Product.countDocuments(queryUsed); if (inheritedCondition === 'new') { if (countNew > 0) buttons.push([Markup.button.callback(`πŸ†• ${c.name} (${countNew})`, `cat_${c.id}_new`)]); } else if (inheritedCondition === 'used') { if (countUsed > 0) buttons.push([Markup.button.callback(`♻️ ${c.name} (${countUsed})`, `cat_${c.id}_used`)]); } else { // No filter yet, show split (or mixed?) // If deep browsing, maybe better to show split again if (countNew > 0) buttons.push([Markup.button.callback(`πŸ†• ${c.name} Yangi (${countNew})`, `cat_${c.id}_new`)]); if (countUsed > 0) buttons.push([Markup.button.callback(`♻️ ${c.name} B/U (${countUsed})`, `cat_${c.id}_used`)]); if (countNew === 0 && countUsed === 0) buttons.push([Markup.button.callback(`${c.name} (0)`, `cat_${c.id}_all`)]); } } buttons.push([Markup.button.callback(ctx.i18n.btn_back, "back_to_cats")]); await ctx.editMessageText(`${ctx.i18n.cat_select}\n**${parent.name}**\n${inheritedCondition ? (inheritedCondition === 'new' ? 'πŸ†• Yangi' : '♻️ B/U') : ''}`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(buttons) } ).catch(() => ctx.reply(ctx.i18n.cat_select, Markup.inlineKeyboard(buttons))); } catch (e) { console.error(e); ctx.reply("Error"); } }; // Show Products (Smart Condition Check) exports.showProducts = async (ctx, catId) => { try { const id = catId.replace('cat_', ''); const category = await Category.findOne({ id: id }); if (!category) return ctx.reply(ctx.i18n.no_cats); // Smart Check: Count products // We treat missing condition as 'new' const countNew = await Product.countDocuments({ category: category.name, $or: [{ condition: 'new' }, { condition: { $exists: false } }] }); const countUsed = await Product.countDocuments({ category: category.name, condition: 'used' }); if (countNew === 0 && countUsed === 0) { return ctx.reply("❌ Bu kategoriyada mahsulotlar yo'q."); } // If only NEW exist -> Show New directly if (countUsed === 0) { return exports.browseCategory(ctx, catId, 0, 0, 'new'); } // If only USED exist -> Show Used directly if (countNew === 0) { return exports.browseCategory(ctx, catId, 0, 0, 'used'); } // If BOTH exist -> Ask user const buttons = [ [ Markup.button.callback(`πŸ†• Yangi (${countNew})`, `cond_${id}_new`), Markup.button.callback(`♻️ B/U (${countUsed})`, `cond_${id}_used`) ], [Markup.button.callback(`🌐 Barchasi (${countNew + countUsed})`, `cond_${id}_all`)], [Markup.button.callback(ctx.i18n.btn_back_nav || "⬅️ Orqaga", "back_to_cats")] ]; // Try edit, fallback to reply await ctx.editMessageText("πŸ”Ž Qaysi turdagi mahsulotlarni ko'rmoqchisiz?", Markup.inlineKeyboard(buttons)) .catch(() => ctx.reply("πŸ”Ž Qaysi turdagi mahsulotlarni ko'rmoqchisiz?", Markup.inlineKeyboard(buttons))); } catch (e) { console.error("Smart Filter Error:", e); ctx.reply("Error in Catalog"); } }; // Filter Handler exports.filterByCondition = async (ctx, catId, condition) => { return exports.browseCategory(ctx, catId, 0, 0, condition); }; // Browse Category (Carousel Logic) exports.browseCategory = async (ctx, catId, prodIndex, mediaIndex = 0, conditionFilter = 'all') => { try { const id = catId.replace('cat_', ''); const category = await Category.findOne({ id: id }); if (!category) return ctx.reply("Kategoriya topilmadi."); let query = { category: category.name }; if (conditionFilter === 'new') { // Treat missing as new query.$or = [{ condition: 'new' }, { condition: { $exists: false } }]; } else if (conditionFilter === 'used') { query.condition = 'used'; } // If 'all', no condition filter needed const products = await Product.find(query); if (!products || products.length === 0) return ctx.reply("❌ Bu kategoriyada mahsulotlar yo'q."); // Handle Product Index Bounds let page = parseInt(prodIndex); if (page < 0) page = products.length - 1; if (page >= products.length) page = 0; const product = products[page]; // Handle Media Index Bounds let mPage = parseInt(mediaIndex); if (!product.media || product.media.length === 0) mPage = 0; else { if (mPage < 0) mPage = product.media.length - 1; if (mPage >= product.media.length) mPage = 0; } const formatPrice = (p) => p.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); const stockText = product.quantity > 0 ? `${ctx.i18n.stock_yes}: ${product.quantity}` : ctx.i18n.stock_no; // Calculate Rating const reviews = product.reviews || []; const avgRating = reviews.length > 0 ? (reviews.reduce((a, b) => a + b.rating, 0) / reviews.length).toFixed(1) : "0.0"; // Prepare Message Content // Price formatting with discount let priceText = `${formatPrice(product.price)} so'm`; if (product.discountPercent > 0 && product.originalPrice) { priceText = `❌ ${formatPrice(product.originalPrice)} | βœ… ${formatPrice(product.price)} so'm (-${product.discountPercent}% πŸ”₯)`; } const caption = `${product.name}\n\n${product.description}\n\n${ctx.i18n.price}: ${priceText}\n${stockText}\n⭐️ ${ctx.i18n.rating}: ${avgRating} (${reviews.length})\n\nπŸ“‚ ${category.name} (${page + 1}/${products.length})`; // Buttons // Navigation: prev/next product. Media index resets to 0. // Format: br_catId_prodIndex_mediaIndex_condition let navigationRow = [ Markup.button.callback("⬅️", `br_${id}_${page - 1}_0_${conditionFilter}`), Markup.button.callback(`${page + 1} / ${products.length}`, "noop"), Markup.button.callback("➑️", `br_${id}_${page + 1}_0_${conditionFilter}`) ]; let mediaRow = []; if (product.media && product.media.length > 1) { mediaRow.push(Markup.button.callback(`πŸ“Έ ${mPage + 1}/${product.media.length}`, `br_${id}_${page}_${mPage + 1}_${conditionFilter}`)); } let actionRow = []; if (product.quantity > 0) { actionRow.push(Markup.button.callback(ctx.i18n.add_to_cart, `add_cart_${product.id}`)); } else { actionRow.push(Markup.button.callback(ctx.i18n.stock_no, `noop`)); } let ratingRow = [Markup.button.callback(ctx.i18n.rate_ask, `rate_ask_${product.id}`)]; let buttons = [ navigationRow, ...(mediaRow.length ? [mediaRow] : []), actionRow, ratingRow, [Markup.button.callback(ctx.i18n.back_to_cats, "back_to_cats")] ]; // Admin Edit Button... (keeping existing logic) const config = require('../config'); if (config.ADMIN_IDS.includes(ctx.from.id.toString())) { buttons.push([Markup.button.callback("✏️ Tahrirlash (Admin)", `edit_prod_${product.id}`)]); } // Send or Edit Message const media = (product.media && product.media.length > 0) ? product.media[mPage] : null; try { await ctx.deleteMessage().catch(() => { }); if (media) { if (media.type === 'video') { await ctx.replyWithVideo(media.file_id, { caption: caption, parse_mode: 'HTML', ...Markup.inlineKeyboard(buttons) }); } else { await ctx.replyWithPhoto(media.file_id, { caption: caption, parse_mode: 'HTML', ...Markup.inlineKeyboard(buttons) }); } } else { await ctx.replyWithHTML(caption, Markup.inlineKeyboard(buttons)); } } catch (e) { console.error("Error sending carousel:", e); ctx.reply("Xatolik bo'ldi"); } } catch (err) { console.error(err); ctx.reply("Xatolik yuz berdi."); } }; // Rating System exports.askRating = (ctx, prodId) => { ctx.answerCbQuery(); const id = prodId; // parse if needed, but rate_ask_ID passes ID string usually // Show 1-5 buttons ctx.reply("Ushbu mahsulotni baholang:", Markup.inlineKeyboard([ [1, 2, 3, 4, 5].map(n => Markup.button.callback(n.toString(), `rate_save_${id}_${n}`)) ])); }; exports.saveRating = async (ctx, prodId, rating) => { try { const id = parseInt(prodId); const r = parseInt(rating); await Product.updateOne( { id: id }, { $push: { reviews: { userId: ctx.from.id, userName: ctx.from.first_name, rating: r } } } ); ctx.deleteMessage(); // remove rating buttons ctx.reply(`βœ… Rahmat! Siz ${r} baho berdingiz.`); } catch (e) { console.error(e); ctx.reply("Xatolik"); } }; // Show Product Details (Direct Link - Keep this for Search results or specific links) exports.showProduct = async (ctx, prodId) => { // ... Existing logic for single product view (from search) ... // Reuse existing logic but maybe simplify or keep as is. // The user instruction was to "replace showProducts" logic. // I need to keep `showProduct` for when they click a search result. // I will replace `showProducts` from the file but I need to preserve `showProduct` below it if I am not targeting it. // The Tool "ReplacementContent" replaces a BLOCK. // I will define `showProduct` again to be safe. try { const id = parseInt(prodId.replace('prod_', '')); const product = await Product.findOne({ id: id }); if (!product) return ctx.reply("Mahsulot topilmadi."); // Price formatting with discount let priceText = `${product.price} so'm`; if (product.discountPercent > 0 && product.originalPrice) { priceText = `❌ ${product.originalPrice} | βœ… ${product.price} so'm (-${product.discountPercent}% πŸ”₯)`; } const caption = `${product.name}\n\n${product.description}\n\nNarxi: ${priceText}`; let buttons = [[Markup.button.callback("πŸ›’ Savatga qo'shish", `add_cart_${product.id}`)]]; // Admin Edit Button const config = require('../config'); if (config.ADMIN_IDS.includes(ctx.from.id.toString())) { buttons.push([Markup.button.callback("✏️ Tahrirlash", `edit_prod_${product.id}`)]); } buttons.push([Markup.button.callback("πŸ”™ Asosiy menyu", "back_to_menu")]); if (product.media && product.media.length > 0) { const m = product.media[0]; if (m.type === 'video') await ctx.replyWithVideo(m.file_id, { caption, parse_mode: 'HTML', ...Markup.inlineKeyboard(buttons) }); else await ctx.replyWithPhoto(m.file_id, { caption, parse_mode: 'HTML', ...Markup.inlineKeyboard(buttons) }); } else { await ctx.replyWithHTML(caption, Markup.inlineKeyboard(buttons)); } } catch (err) { console.error(err); ctx.reply("Xatolik: " + err.message); } }; // Helper: Format Price const formatPrice = (price) => { return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); }; // ... (In showProducts/browseCategory/showProduct - I will do this via multiple replacements or just one big one if they are close, but they are scattered.) // Let's stick to adding helper and updating addToCart first. // Add to Cart exports.addToCart = async (ctx, prodId) => { try { const id = parseInt(prodId.replace('add_cart_', '')); const product = await Product.findOne({ id: id }); if (!product) return ctx.answerCbQuery("Mahsulot topilmadi"); if (!ctx.session.cart) ctx.session.cart = []; // Check stock const currentInCart = ctx.session.cart.find(item => item.id === id); const currentCount = currentInCart ? currentInCart.count : 0; if (currentCount + 1 > (product.quantity || 0)) { return ctx.answerCbQuery(`⚠️ Omborda bor-yo'g'i ${product.quantity} ta qoldi!`, { show_alert: true }); } if (currentInCart) { currentInCart.count++; } else { const prodObj = product.toObject(); ctx.session.cart.push({ ...prodObj, count: 1 }); } // Calculate Cart Total for Feedback const total = ctx.session.cart.reduce((acc, item) => acc + (item.price * item.count), 0); ctx.answerCbQuery(`βœ… Qo'shildi!\nSavatda: ${formatPrice(total)} so'm`, { show_alert: false }); // show_alert false = toast } catch (err) { console.error(err); } }; // Show Contact Info exports.showContact = (ctx) => { // Ensure i18n const i18n = ctx.i18n || require('../locales').uz; // Reply with text and TWO buttons as requested ctx.reply(i18n.contact_info, Markup.inlineKeyboard([ [Markup.button.url(i18n.admin_contact_prod, "https://t.me/isfandiyor_3")], [Markup.button.url(i18n.admin_contact_bot, "https://t.me/IBROHM_7")] ])); }; // Show Cart (Interactive) exports.showCart = (ctx) => { const i18n = ctx.i18n || require('../locales').uz; const cart = ctx.session.cart || []; if (cart.length === 0) { // If message to edit exists if (ctx.callbackQuery) { return ctx.editMessageText(i18n.cart_empty).catch(() => ctx.reply(i18n.cart_empty)); } return ctx.reply(i18n.cart_empty); } const formatPrice = (price) => price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); let text = `${i18n.cart_title}\n\n`; let total = 0; const buttons = []; cart.forEach((item, index) => { const itemTotal = item.price * item.count; total += itemTotal; text += `${index + 1}. ${item.name}\n${item.count} x ${formatPrice(item.price)} = ${formatPrice(itemTotal)} ${i18n.currency}\n\n`; // Interactive Row: [ - ] [ 1 ] [ + ] [ πŸ—‘ ] buttons.push([ Markup.button.callback("βž–", `cart_decr_${index}`), Markup.button.callback(`${item.count} ta`, `noop`), Markup.button.callback("βž•", `cart_incr_${index}`), Markup.button.callback("πŸ—‘", `cart_del_${index}`) ]); }); text += `${i18n.cart_total} ${formatPrice(total)} ${i18n.currency}`; // Control Actions buttons.push([ Markup.button.callback(i18n.btn_order, "checkout"), Markup.button.callback(i18n.btn_clear, "clear_cart") ]); buttons.push([Markup.button.callback(i18n.back, "back_to_menu")]); const keyboard = Markup.inlineKeyboard(buttons); if (ctx.callbackQuery) { ctx.editMessageText(text, { parse_mode: 'HTML', ...keyboard }).catch(() => ctx.replyWithHTML(text, keyboard)); } else { ctx.replyWithHTML(text, keyboard); } }; // Cart Actions Handling exports.cartAction = async (ctx, action, indexStr) => { const index = parseInt(indexStr); const cart = ctx.session.cart || []; const i18n = ctx.i18n || require('../locales').uz; if (!cart[index]) return ctx.answerCbQuery(i18n.error || "Xatolik", { show_alert: true }); const item = cart[index]; const product = await Product.findOne({ id: item.id }); if (action === 'incr') { const stock = product ? product.quantity : 0; if (item.count >= stock) { return ctx.answerCbQuery( i18n.cart_stock_err.replace('{item}', item.name).replace('{stock}', stock), { show_alert: true } ); } item.count++; } else if (action === 'decr') { if (item.count > 1) { item.count--; } else { // Ask to remove? Or just remove? Let's just remove logic or do nothing // Usually decrementing 1 -> 0 should remove or stop at 1. Let's stop at 1. User can click trash. return ctx.answerCbQuery(); } } else if (action === 'del') { cart.splice(index, 1); } ctx.session.cart = cart; // exports.showCart(ctx); // Recursive refresh // Instead of calling showCart (which creates new scope), let's just re-run the logic or call a refresh helper. // showCart is designed to handle edit, so calling it is fine. exports.showCart(ctx); }; // Clear Cart exports.clearCart = (ctx) => { const i18n = ctx.i18n || require('../locales').uz; ctx.session.cart = []; ctx.answerCbQuery(i18n.cart_empty); if (ctx.callbackQuery) { ctx.editMessageText(i18n.cart_empty).catch(() => ctx.reply(i18n.cart_empty)); } else { ctx.reply(i18n.cart_empty); } }; // Checkout Steps exports.startCheckout = (ctx) => { const cart = ctx.session.cart || []; if (cart.length === 0) return ctx.answerCbQuery("Savat bo'sh!"); ctx.deleteMessage(); // Clean up cart view ctx.reply("πŸ“ž Iltimos, aloqa uchun telefon raqamingizni yuboring:", Markup.keyboard([ Markup.button.contactRequest("πŸ“± Telefon raqamni yuborish") ]).resize().oneTime()); }; // --- NEW FEATURES --- // 1. Search Functionality exports.startSearch = (ctx) => { ctx.reply("πŸ” Mahsulot nomini yozib yuboring:", Markup.keyboard([['❌ Bekor qilish']]).resize()); // Note: We need a way to track that user is in "search mode". // For simplicity, we can use session state. ctx.session.isSearching = true; }; exports.handleSearch = async (ctx) => { if (!ctx.session.isSearching) return false; // Not in search mode const query = ctx.message.text; // Exit search on specific commands if (query === '/start' || query === 'πŸ”™ Orqaga' || query === '❌ Bekor qilish') { ctx.session.isSearching = false; exports.start(ctx, "Qidiruv bekor qilindi."); return true; } try { // Case-insensitive regex search const products = await Product.find({ name: { $regex: query, $options: 'i' } }).limit(10); if (products.length === 0) { ctx.reply(ctx.i18n.search_empty); return true; } const buttons = products.map(p => { const cond = p.condition === 'used' ? '♻️' : 'πŸ†•'; // Show price in button too return [Markup.button.callback(`${cond} ${p.name}`, `prod_${p.id}`)]; }); ctx.reply(`${ctx.i18n.search_results} "${query}"`, Markup.inlineKeyboard(buttons)); ctx.session.isSearching = false; // Reset after successful search result // Or keep it true to allow multiple searches? Let's reset to be safe and they can clear menu. // Actually, better UX: Don't reset, allow them to search again if not found? // Let's reset here so keyboard returns to normal if they click a product. } catch (err) { console.error(err); ctx.reply("Qidiruvda xatolik."); } return true; // We handled the message }; // 2. Order History exports.showOrderHistory = async (ctx) => { try { const orders = await Order.find({ userId: ctx.from.id }).sort({ createdAt: -1 }).limit(10); if (!orders || orders.length === 0) { return ctx.reply(ctx.i18n.no_orders); } let text = `${ctx.i18n.order_history_title}\n\n${ctx.i18n.order_history}`; // Create a list of buttons for orders like: πŸ“¦ #1234 | 120 000 | βœ… const buttons = orders.map(o => { let statusIcon = 'πŸ“¦'; switch (o.status) { case 'new': statusIcon = ctx.i18n.status_new; break; case 'accepted': statusIcon = ctx.i18n.status_accepted; break; case 'canceled': statusIcon = ctx.i18n.status_canceled; break; case 'shipping': statusIcon = ctx.i18n.status_shipping; break; case 'delivered': statusIcon = ctx.i18n.status_delivered; break; default: statusIcon = o.status; } return [Markup.button.callback(`${statusIcon} | #${o.id} | ${formatPrice(o.total)}`, `my_order_${o.id}`)]; }); buttons.push([Markup.button.callback(ctx.i18n.back_to_menu || "πŸ”™ Asosiy menyu", "back_to_menu")]); ctx.replyWithHTML(text, Markup.inlineKeyboard(buttons)); } catch (err) { console.error(err); ctx.reply(ctx.i18n.error_generic); } }; exports.showOrderDetails = async (ctx, orderId) => { try { const id = parseInt(orderId); const order = await Order.findOne({ id: id, userId: ctx.from.id }); // Security check: must match userId if (!order) return ctx.answerCbQuery(ctx.i18n.error_generic); let statusText = order.status; switch (order.status) { case 'new': statusText = ctx.i18n.status_new; break; case 'accepted': statusText = ctx.i18n.status_accepted; break; case 'canceled': statusText = ctx.i18n.status_canceled; break; case 'shipping': statusText = ctx.i18n.status_shipping; break; case 'delivered': statusText = ctx.i18n.status_delivered; break; } let text = `${ctx.i18n.order_title}${order.id}**\n` + `${ctx.i18n.date}: ${new Date(order.createdAt).toLocaleString()}\n` + `${ctx.i18n.order_details_pay} ${order.paymentMethod || ctx.i18n.payment_cash}\n` + `${ctx.i18n.order_details_status} ${statusText}\n\n` + `${ctx.i18n.items}\n`; order.items.forEach(i => { text += `- ${i.name} (${i.count}x) - ${formatPrice(i.price)} ${ctx.i18n.currency}\n`; }); text += `\n${ctx.i18n.total_label} ${formatPrice(order.total)} ${ctx.i18n.currency}`; const buttons = []; // Allow cancellation only if status is 'new' if (order.status === 'new') { buttons.push([Markup.button.callback(ctx.i18n.btn_reject, `user_cancel_order_${order.id}`)]); } buttons.push([Markup.button.callback(ctx.i18n.back, "my_orders_list")]); ctx.editMessageText(text, { parse_mode: 'HTML', ...Markup.inlineKeyboard(buttons) }) .catch(() => ctx.replyWithHTML(text, Markup.inlineKeyboard(buttons))); } catch (err) { console.error(err); ctx.reply(ctx.i18n.error_generic); } }; // User Cancel Order (Restock) exports.cancelUserOrder = async (ctx, orderId) => { try { const id = parseInt(orderId); const order = await Order.findOne({ id: id, userId: ctx.from.id }); if (!order) return ctx.answerCbQuery(ctx.i18n.error_generic); if (order.status !== 'new') return ctx.answerCbQuery(ctx.i18n.error_generic); // Restock Logic const Product = require('../models/Product'); for (const item of order.items) { await Product.updateOne({ id: item.id }, { $inc: { quantity: item.count } }); } await Order.updateOne({ id: id }, { status: 'canceled' }); ctx.answerCbQuery(ctx.i18n.alert_canceled); // Notify Admin about cancellation (Optional but good logic) const config = require('../config'); for (const adminId of config.ADMIN_IDS) { ctx.telegram.sendMessage(adminId, `⚠️ Buyurtma bekor qilindi!\n#${id} bekor qilindi (Foydalanuvchi tomonidan).`, { parse_mode: 'HTML' }).catch(e => { }); } // Refresh View exports.showOrderDetails(ctx, id); } catch (err) { console.error(err); ctx.reply(ctx.i18n.error_generic); } };