Spaces:
Paused
Paused
| const { Markup } = require('telegraf'); | |
| const User = require('../models/User'); | |
| const Product = require('../models/Product'); | |
| const Order = require('../models/Order'); | |
| const ExcelJS = require('exceljs'); | |
| // Helper to get Dashboard Buttons | |
| const getDashboardButtons = () => { | |
| return [ | |
| [Markup.button.callback("📊 Statistika", "admin_stats"), Markup.button.callback("📦 Yangi buyurtmalar", "admin_orders_new")], | |
| [Markup.button.callback("➕ Mahsulot qo'shish", "admin_add_product"), Markup.button.callback("🗑 Mahsulot o'chirish", "admin_delete_product")], | |
| [Markup.button.callback("✏️ Mahsulotni tahrirlash", "admin_edit_product_list"), Markup.button.callback("👥 Foydalanuvchilar", "admin_users")], | |
| [Markup.button.callback("📉 Excel Export", "admin_excel_export"), Markup.button.callback("📢 Reklama yuborish", "admin_broadcast")], | |
| [Markup.button.callback("🔥 Aksiya (Flash Sale)", "admin_flash_sale"), Markup.button.callback("📦 Ombor Boshqaruvi", "admin_inventory")], | |
| [Markup.button.callback("⚙️ Do'kon Sozlamalari", "admin_settings"), Markup.button.callback("🔑 API Integratsiya", "admin_api")] // Added API Button | |
| ]; | |
| }; | |
| // Admin Dashboard Menu | |
| exports.showDashboard = (ctx) => { | |
| // Middleware ensures ctx.i18n is present | |
| const i18n = ctx.i18n; | |
| const text = i18n.admin.dash_title; | |
| // Dynamic Buttons | |
| const buttons = [ | |
| [Markup.button.callback(i18n.admin.btn_stats, "admin_stats"), Markup.button.callback(i18n.admin.btn_new_orders, "admin_orders_new")], | |
| [Markup.button.callback(i18n.admin.btn_add_prod, "admin_add_product"), Markup.button.callback(i18n.admin.btn_del_prod, "admin_delete_product")], | |
| [Markup.button.callback(i18n.admin.btn_edit_prod, "admin_edit_product_list"), Markup.button.callback(i18n.admin.btn_users, "admin_users")], | |
| [Markup.button.callback(i18n.admin.btn_excel, "admin_excel_export"), Markup.button.callback(i18n.admin.btn_broadcast, "admin_broadcast")], | |
| [Markup.button.callback(i18n.admin.btn_flash, "admin_flash_sale"), Markup.button.callback(i18n.admin.btn_inv, "admin_inventory")], | |
| [Markup.button.callback(i18n.admin.btn_settings, "admin_settings"), Markup.button.callback(i18n.admin.btn_api, "admin_api")] | |
| ]; | |
| const keyboard = Markup.inlineKeyboard(buttons); | |
| try { | |
| if (ctx.callbackQuery) { | |
| ctx.editMessageText(text, { parse_mode: 'Markdown', ...keyboard }).catch(e => ctx.replyWithMarkdown(text, keyboard)); | |
| } else { | |
| ctx.replyWithMarkdown(text, keyboard); | |
| } | |
| } catch (e) { | |
| ctx.replyWithMarkdown(text, keyboard); | |
| } | |
| }; | |
| // Statistics | |
| exports.showStats = async (ctx) => { | |
| try { | |
| const i18n = ctx.i18n; | |
| const userCount = await User.countDocuments(); | |
| const productCount = await Product.countDocuments(); | |
| const orders = await Order.find(); | |
| const revenue = orders | |
| .filter(o => o.status === 'completed' || o.status === 'delivered') | |
| .reduce((sum, o) => sum + o.total, 0); | |
| const text = `${i18n.admin.stats_title}\n\n` + | |
| `${i18n.admin.stats_users}: ${userCount}\n` + | |
| `${i18n.admin.stats_prods}: ${productCount}\n` + | |
| `${i18n.admin.stats_orders}: ${orders.length}\n` + | |
| `${i18n.admin.stats_rev}: ${revenue.toLocaleString()} ${i18n.currency}`; | |
| const keyboard = Markup.inlineKeyboard([[Markup.button.callback(i18n.admin.back, "admin_dashboard")]]); | |
| ctx.editMessageText(text, { parse_mode: 'HTML', ...keyboard }) | |
| .catch(e => ctx.replyWithHTML(text, keyboard)); | |
| } catch (err) { | |
| console.error(err); | |
| ctx.answerCbQuery(ctx.i18n.admin.error); | |
| } | |
| }; | |
| // Show New Orders | |
| exports.showNewOrders = async (ctx) => { | |
| try { | |
| const i18n = ctx.i18n; | |
| const orders = await Order.find({ status: 'new' }); | |
| const backBtn = [Markup.button.callback(i18n.admin.back, "admin_dashboard")]; | |
| if (!orders || orders.length === 0) { | |
| return ctx.editMessageText(i18n.admin.no_new_orders, Markup.inlineKeyboard([backBtn])) | |
| .catch(e => ctx.reply(i18n.admin.no_new_orders, Markup.inlineKeyboard([backBtn]))); | |
| } | |
| const orderButtons = orders.map(o => [Markup.button.callback(`#${o.id} - ${o.total.toLocaleString()} ${i18n.currency}`, `admin_order_${o.id}`)]); | |
| orderButtons.push(backBtn); | |
| const text = `${i18n.admin.btn_new_orders}: ${orders.length}`; | |
| const keyboard = Markup.inlineKeyboard(orderButtons); | |
| ctx.editMessageText(text, { parse_mode: 'Markdown', ...keyboard }) | |
| .catch(e => ctx.replyWithMarkdown(text, keyboard)); | |
| } catch (err) { | |
| console.error(err); | |
| ctx.answerCbQuery(ctx.i18n.admin.error); | |
| } | |
| }; | |
| // View Single Order Details | |
| exports.viewOrder = async (ctx, orderId) => { | |
| try { | |
| const i18n = ctx.i18n; | |
| const id = parseInt(orderId); | |
| if (isNaN(id)) return ctx.answerCbQuery("❌ Invalid Order ID"); | |
| const order = await Order.findOne({ id: id }); | |
| if (!order) return ctx.answerCbQuery("Buyurtma topilmadi."); // Low Priority to localize this tiny string fallback | |
| let text = `${i18n.admin.order_title}${order.id}**\n` + | |
| `${i18n.admin.client}: ${order.user}\n` + | |
| `${i18n.admin.tel}: ${order.phone}\n` + | |
| `${i18n.admin.date}: ${new Date(order.createdAt).toLocaleString()}\n` + | |
| `${i18n.admin.status_label}: ${order.status.toUpperCase()}\n\n` + | |
| `${i18n.admin.items}\n`; | |
| order.items.forEach(i => { | |
| text += `- ${i.name} (${i.count}x) - ${i.price.toLocaleString()} ${i18n.currency}\n`; | |
| }); | |
| text += `\n${i18n.admin.total_label} ${order.total.toLocaleString()} ${i18n.currency}`; | |
| if (order.location) { | |
| ctx.replyWithLocation(order.location.latitude, order.location.longitude); | |
| } | |
| // Dynamic Buttons based on Status | |
| let actionButtons = []; | |
| if (order.status === 'new') { | |
| actionButtons = [ | |
| [Markup.button.callback(i18n.admin.btn_accept, `order_accept_${order.id}`), Markup.button.callback(i18n.admin.btn_reject, `order_reject_${order.id}`)] | |
| ]; | |
| } else if (order.status === 'accepted') { | |
| actionButtons = [ | |
| [Markup.button.callback(i18n.admin.btn_invoice, `gen_invoice_${order.id}`)], | |
| [Markup.button.callback(i18n.admin.btn_ship, `order_ship_${order.id}`)], | |
| [Markup.button.callback(i18n.admin.btn_reject, `order_reject_${order.id}`)] | |
| ]; | |
| } else if (order.status === 'shipping') { | |
| actionButtons = [ | |
| [Markup.button.callback(i18n.admin.btn_deliver, `order_deliver_${order.id}`)], | |
| [Markup.button.callback(i18n.admin.btn_reject, `order_reject_${order.id}`)] | |
| ]; | |
| } else if (order.status === 'delivered') { | |
| actionButtons = [ | |
| [Markup.button.callback(i18n.admin.btn_archive, "admin_orders_new")] | |
| ]; | |
| } | |
| actionButtons.push([Markup.button.callback(i18n.admin.back, "admin_orders_new")]); | |
| const contextKeyboard = Markup.inlineKeyboard(actionButtons); | |
| ctx.editMessageText(text, { parse_mode: 'HTML', ...contextKeyboard }) | |
| .catch(e => ctx.replyWithHTML(text, contextKeyboard)); | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| // Helper: Generate Invoice Text | |
| const generateInvoice = (order) => { | |
| const date = new Date(order.createdAt).toLocaleString('uz-UZ'); | |
| let items = ""; | |
| order.items.forEach(i => { | |
| items += `- ${i.name} (${i.count}x) : ${i.price.toLocaleString()} so'm\n`; | |
| }); | |
| return `🧾 **CHECK**\n\n` + | |
| `🆔 Buyurtma: #${order.id}\n` + | |
| `📅 Sana: ${date}\n` + | |
| `👤 Mijoz: ${order.user} (${order.phone})\n` + | |
| `💳 To'lov: ${order.paymentMethod === 'click' ? 'Click/Karta' : 'Naqd'}\n` + | |
| `------------------------------\n` + | |
| `${items}` + | |
| `------------------------------\n` + | |
| `💰 **JAMI: ${order.total.toLocaleString()} so'm**\n\n` + | |
| `✅ Status: TO'LANDI`; | |
| }; | |
| // Accept Order | |
| exports.acceptOrder = async (ctx, orderId) => { | |
| try { | |
| const id = parseInt(orderId); | |
| await Order.updateOne({ id: id }, { status: 'accepted' }); | |
| const order = await Order.findOne({ id: id }); | |
| ctx.answerCbQuery(ctx.i18n.admin.alert_accepted); | |
| // Notify User | |
| if (order) { | |
| const User = require('../models/User'); | |
| const locales = require('../locales'); | |
| const user = await User.findOne({ id: order.userId }); | |
| const lang = (user && user.language) ? user.language : 'uz'; | |
| const i18n = locales[lang] || locales.uz; | |
| ctx.telegram.sendMessage(order.userId, `${i18n.status_accepted}\n#${id} ${i18n.status_proccess || "Accepted"}.`); | |
| } | |
| // Refresh View | |
| exports.viewOrder(ctx, id); | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| // Ship Order | |
| exports.shipOrder = async (ctx, orderId) => { | |
| try { | |
| const id = parseInt(orderId); | |
| await Order.updateOne({ id: id }, { status: 'shipping' }); | |
| const order = await Order.findOne({ id: id }); | |
| ctx.answerCbQuery(ctx.i18n.admin.alert_shipping); | |
| // Notify User | |
| if (order) { | |
| const User = require('../models/User'); | |
| const locales = require('../locales'); | |
| const user = await User.findOne({ id: order.userId }); | |
| const lang = (user && user.language) ? user.language : 'uz'; | |
| const i18n = locales[lang] || locales.uz; | |
| ctx.telegram.sendMessage(order.userId, `${i18n.status_shipping}\n#${id}`); | |
| } | |
| exports.viewOrder(ctx, id); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| }; | |
| // Deliver Order | |
| exports.deliverOrder = async (ctx, orderId) => { | |
| try { | |
| const id = parseInt(orderId); | |
| await Order.updateOne({ id: id }, { status: 'delivered' }); | |
| const order = await Order.findOne({ id: id }); | |
| ctx.answerCbQuery(ctx.i18n.admin.alert_delivered); | |
| // Notify User | |
| if (order) { | |
| const User = require('../models/User'); | |
| const locales = require('../locales'); | |
| const user = await User.findOne({ id: order.userId }); | |
| const lang = (user && user.language) ? user.language : 'uz'; | |
| const i18n = locales[lang] || locales.uz; | |
| ctx.telegram.sendMessage(order.userId, `${i18n.status_delivered}\n#${id}`); | |
| } | |
| exports.viewOrder(ctx, id); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| }; | |
| // Generate Invoice Action Handler | |
| exports.showInvoice = async (ctx, orderId) => { | |
| try { | |
| const id = parseInt(orderId); | |
| const order = await Order.findOne({ id: id }); | |
| if (!order) return ctx.answerCbQuery("Buyurtma yo'q"); | |
| const invoice = generateInvoice(order); | |
| await ctx.replyWithMarkdown(invoice); | |
| ctx.answerCbQuery("Chek tayyor!"); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| }; | |
| exports.toggleBlockUser = async (ctx, userId, isBlock) => { | |
| try { | |
| await User.updateOne({ id: userId }, { isBlocked: isBlock }); | |
| const msg = isBlock ? "Foydalanuvchi bloklandi 🚫" : "Foydalanuvchi blokdan chiqarildi ✅"; | |
| ctx.answerCbQuery(msg); | |
| exports.manageUser(ctx, userId); // Refresh view | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| }; | |
| // --- API Key Management --- | |
| const crypto = require('crypto'); | |
| const Settings = require('../models/Settings'); | |
| exports.showApiMenu = async (ctx) => { | |
| try { | |
| const apiKeySetting = await Settings.findOne({ key: 'api_secret_key' }); | |
| const apiKey = apiKeySetting ? apiKeySetting.value : "⚠️ Mavjud emas"; | |
| let text = `🔑 **API Integratsiya Sozlamalari**\n\n` + | |
| `Sizning API Kalitingiz (Secret Key):\n` + | |
| `<code>${apiKey}</code>\n\n` + | |
| `⚠️ **Eslatma:** Bu kalit orqali kelajakdagi APK mobil ilovani bot bazasiga ulashingiz mumkin. Kalitni birovga bermang!`; | |
| const keyboard = Markup.inlineKeyboard([ | |
| [Markup.button.callback("🔄 Yangi Kalit Yaratish", "admin_api_generate")], | |
| [Markup.button.callback("🔙 Orqaga", "admin_dashboard")] | |
| ]); | |
| ctx.editMessageText(text, { parse_mode: 'HTML', ...keyboard }) | |
| .catch(e => ctx.replyWithHTML(text, keyboard)); | |
| } catch (e) { | |
| console.error(e); | |
| ctx.reply("Xatolik"); | |
| } | |
| }; | |
| exports.generateApiKey = async (ctx) => { | |
| try { | |
| // Generate new key | |
| const newKey = "sk_live_" + crypto.randomBytes(16).toString('hex'); | |
| await Settings.findOneAndUpdate( | |
| { key: 'api_secret_key' }, | |
| { value: newKey }, | |
| { upsert: true, new: true } | |
| ); | |
| ctx.answerCbQuery("Yangi kalit yaratildi ✅"); | |
| exports.showApiMenu(ctx); | |
| } catch (e) { | |
| console.error(e); | |
| ctx.answerCbQuery("Xatolik"); | |
| } | |
| }; | |
| // --- Mass Price Update --- | |
| exports.startMassPriceUpdate = (ctx) => { | |
| ctx.reply("📈 **Narxlarni ommaviy o'zgartirish**\n\nBarcha mahsulotlar narxini foiz (%) hisobida oshirish yoki kamaytirish mumkin.\n\nMasalan:\n+10 => Narxlar 10% ga oshadi.\n-5 => Narxlar 5% ga kamayadi.\n\nIltimos, foizni yozing:", Markup.keyboard([['❌ Bekor qilish']]).resize()); | |
| ctx.session.isMassUpdating = true; | |
| }; | |
| exports.handleMassUpdate = async (ctx) => { | |
| if (!ctx.session.isMassUpdating) return false; | |
| const text = ctx.message.text; | |
| if (text === '❌ Bekor qilish') { | |
| ctx.session.isMassUpdating = false; | |
| exports.showDashboard(ctx); // Return to dashboard | |
| return true; | |
| } | |
| const percent = parseFloat(text); | |
| if (isNaN(percent)) { | |
| ctx.reply("⚠️ Iltimos, raqam yozing (masalan: 10 yoki -5)."); | |
| return true; | |
| } | |
| try { | |
| const products = await Product.find(); | |
| let changed = 0; | |
| for (const p of products) { | |
| const oldPrice = p.price; | |
| const newPrice = Math.round(oldPrice * (1 + percent / 100)); // Simple formula | |
| await Product.updateOne({ id: p.id }, { price: newPrice }); | |
| changed++; | |
| } | |
| ctx.reply(`✅ **Muvaffaqiyatli!**\n\n${changed} ta mahsulot narxi ${percent}% ga o'zgartirildi.`); | |
| ctx.session.isMassUpdating = false; | |
| exports.showDashboard(ctx); | |
| } catch (e) { | |
| console.error(e); | |
| ctx.reply("Xatolik yuz berdi."); | |
| } | |
| return true; | |
| }; | |
| // Reject Order (Refund Logic) | |
| exports.rejectOrder = async (ctx, orderId) => { | |
| try { | |
| const id = parseInt(orderId); | |
| const order = await Order.findOne({ id: id }); | |
| if (!order) return ctx.answerCbQuery("Buyurtma topilmadi"); | |
| // Restore Stock Logic (Automatic Refund) | |
| if (order.status !== 'canceled') { | |
| 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("Buyurtma bekor qilindi ❌"); | |
| // Notify User | |
| if (order) { | |
| const User = require('../models/User'); | |
| const locales = require('../locales'); | |
| const user = await User.findOne({ id: order.userId }); | |
| const lang = (user && user.language) ? user.language : 'uz'; | |
| const i18n = locales[lang] || locales.uz; | |
| ctx.telegram.sendMessage(order.userId, `${i18n.status_canceled}\nBuyurtma #${id} bekor qilindi.\n(Sabab bo'lsa admin bilan bog'laning)`); | |
| } | |
| exports.showNewOrders(ctx); | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| // Edit Product List | |
| exports.showEditProductList = async (ctx) => { | |
| try { | |
| const i18n = ctx.i18n; | |
| const products = await Product.find(); | |
| const backBtn = [Markup.button.callback(i18n.admin.back, "admin_dashboard")]; | |
| if (!products || products.length === 0) { | |
| return ctx.editMessageText(i18n.admin.no_edit_prod, Markup.inlineKeyboard([backBtn])) | |
| .catch(e => ctx.reply(i18n.admin.no_edit_prod, Markup.inlineKeyboard([backBtn]))); | |
| } | |
| const buttons = products.map(p => [Markup.button.callback(`✏️ ${p.name || 'Nomsiz'}`, `edit_prod_${p.id}`)]); | |
| buttons.push(backBtn); | |
| ctx.editMessageText(i18n.admin.select_edit, Markup.inlineKeyboard(buttons)) | |
| .catch(e => ctx.reply(i18n.admin.select_edit, Markup.inlineKeyboard(buttons))); | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| // Delete Product List | |
| exports.showDeleteProductList = async (ctx) => { | |
| try { | |
| const i18n = ctx.i18n; | |
| const products = await Product.find(); | |
| const backBtn = [Markup.button.callback(i18n.admin.back, "admin_dashboard")]; | |
| if (!products || products.length === 0) { | |
| return ctx.editMessageText(i18n.admin.no_del_prod, Markup.inlineKeyboard([backBtn])) | |
| .catch(e => ctx.reply(i18n.admin.no_del_prod, Markup.inlineKeyboard([backBtn]))); | |
| } | |
| const buttons = products.map(p => [Markup.button.callback(`🗑 ${p.name}`, `delete_prod_${p.id}`)]); | |
| buttons.push(backBtn); | |
| ctx.editMessageText(i18n.admin.select_del, Markup.inlineKeyboard(buttons)) | |
| .catch(e => ctx.reply(i18n.admin.select_del, Markup.inlineKeyboard(buttons))); | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| exports.deleteProduct = async (ctx, prodId) => { | |
| try { | |
| const i18n = ctx.i18n; | |
| const id = parseInt(prodId); | |
| const result = await Product.findOneAndDelete({ id: id }); | |
| if (result) { | |
| ctx.answerCbQuery(i18n.admin.del_success, { show_alert: true }); | |
| } else { | |
| ctx.answerCbQuery(i18n.admin.del_fail, { show_alert: true }); | |
| } | |
| // Refresh list | |
| exports.showDeleteProductList(ctx); | |
| } catch (err) { | |
| console.error("Delete Error:", err); | |
| ctx.answerCbQuery(ctx.i18n.admin.error); | |
| } | |
| }; | |
| // Excel Export | |
| exports.exportOrders = async (ctx) => { | |
| try { | |
| ctx.reply("📉 Fayl tayyorlanmoqda..."); | |
| const orders = await Order.find().sort({ createdAt: -1 }); | |
| const workbook = new ExcelJS.Workbook(); | |
| const worksheet = workbook.addWorksheet('Buyurtmalar'); | |
| worksheet.columns = [ | |
| { header: 'ID', key: 'id', width: 15 }, | |
| { header: 'Mijoz', key: 'user', width: 20 }, | |
| { header: 'Telefon', key: 'phone', width: 15 }, | |
| { header: 'Manzil', key: 'address', width: 30 }, | |
| { header: 'Yetkazib berish', key: 'delivery', width: 15 }, | |
| { header: 'To\'lov', key: 'payment', width: 10 }, | |
| { header: 'Jami (so\'m)', key: 'total', width: 15 }, | |
| { header: 'Status', key: 'status', width: 10 }, | |
| { header: 'Vaqt', key: 'date', width: 20 }, | |
| { header: 'Mahsulotlar', key: 'items', width: 50 }, | |
| { header: 'Izoh', key: 'comment', width: 30 } | |
| ]; | |
| orders.forEach(order => { | |
| const itemsStr = order.items.map(i => `${i.name} (${i.count}x)`).join(', '); | |
| const locStr = order.location ? `${order.location.latitude}, ${order.location.longitude}` : 'Olib ketish'; | |
| worksheet.addRow({ | |
| id: order.id, | |
| user: order.user, | |
| phone: order.phone, | |
| address: locStr, | |
| delivery: order.deliveryMethod, | |
| payment: order.paymentMethod, | |
| total: order.total, | |
| status: order.status, | |
| date: new Date(order.createdAt).toLocaleString(), | |
| items: itemsStr, | |
| comment: order.comment || '' | |
| }); | |
| }); | |
| const buffer = await workbook.xlsx.writeBuffer(); | |
| await ctx.replyWithDocument({ source: buffer, filename: `Buyurtmalar_${new Date().toLocaleDateString()}.xlsx` }); | |
| } catch (err) { | |
| console.error("Excel Export Error:", err); | |
| ctx.reply("Fayl yaratishda xato bo'ldi."); | |
| } | |
| }; | |