telegram-shop-api / src /controllers /adminController.js
Deploy Bot
Fix: Add DB Self-Healing and CastError Validation
bfde415
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.");
}
};