telegram-shop-api / src /scenes /admin /inventory.js
Deploy Bot
Complete_Localization_Maximal_Logic
298f5d2
const { Scenes, Markup } = require('telegraf');
const Product = require('../../models/Product');
const ExcelJS = require('exceljs');
const axios = require('axios');
const inventoryScene = new Scenes.WizardScene(
'admin_inventory',
// Step 1: Menu
async (ctx) => {
// Inject i18n
if (!ctx.i18n) {
const User = require('../../models/User');
const locales = require('../../locales');
const user = await User.findOne({ id: ctx.from.id });
const lang = (user && user.language) ? user.language : 'uz';
ctx.i18n = locales[lang] || locales.uz;
}
const i18n = ctx.i18n;
const text = i18n.admin.inv_title;
const buttons = [
[Markup.button.callback(i18n.admin.inv_mass_price, "mass_price_update")],
[Markup.button.callback(i18n.admin.inv_excel, "excel_import")],
[Markup.button.callback(i18n.admin.back, "back_dashboard")]
];
if (ctx.callbackQuery) {
await ctx.editMessageText(text, { parse_mode: 'Markdown', ...Markup.inlineKeyboard(buttons) });
} else {
await ctx.replyWithMarkdown(text, Markup.inlineKeyboard(buttons));
}
return ctx.wizard.next();
},
// Step 2: Handle Choice
async (ctx) => {
// Ensure i18n
if (!ctx.i18n) {
const locales = require('../../locales');
ctx.i18n = locales.uz; // Fallback
}
const i18n = ctx.i18n;
if (!ctx.callbackQuery && !ctx.message) return;
const txt = ctx.message ? ctx.message.text : '';
if (txt === '/start' || txt === i18n.admin.btn_reject || txt === '❌ Bekor qilish') return ctx.scene.leave();
if (ctx.callbackQuery) {
const data = ctx.callbackQuery.data;
if (data === 'back_dashboard') {
await ctx.scene.leave();
const adminController = require('../../controllers/adminController');
// Re-fetch user logic inside controller usually handles i18n, but here we call it directly.
// adminController uses ctx.i18n, so it's fine.
return adminController.showDashboard(ctx);
}
if (data === 'mass_price_update') {
await ctx.reply(i18n.admin.inv_price_prompt, { parse_mode: 'Markdown', ...Markup.keyboard([[i18n.admin.btn_reject]]).resize() });
ctx.wizard.state.action = 'mass_price';
return ctx.wizard.next();
}
if (data === 'excel_import') {
await ctx.reply(i18n.admin.inv_excel_prompt, Markup.keyboard([[i18n.admin.btn_reject]]).resize());
ctx.wizard.state.action = 'excel_import';
return ctx.wizard.next();
}
}
},
// Step 3: Execution
async (ctx) => {
// Ensure i18n
if (!ctx.i18n) {
const locales = require('../../locales');
ctx.i18n = locales.uz;
}
const i18n = ctx.i18n;
const txt = ctx.message ? ctx.message.text : '';
if (txt === i18n.admin.btn_reject || txt === '❌ Bekor qilish') {
await ctx.reply(i18n.admin.back || "Bekor qilindi.", Markup.removeKeyboard());
await ctx.scene.leave();
const adminController = require('../../controllers/adminController');
return adminController.showDashboard(ctx);
}
// --- Mass Price Update Logic ---
if (ctx.wizard.state.action === 'mass_price') {
const percent = parseInt(txt);
if (isNaN(percent) || percent === 0) {
await ctx.reply(i18n.admin.error_num || "Error");
return;
}
await ctx.reply(i18n.admin.broadcast_sending || "⏳...");
const products = await Product.find({});
let count = 0;
for (let p of products) {
try {
if (!p.id || !p.name) continue; // Skip invalid
const change = (p.price * percent) / 100;
p.price = Math.round(p.price + change);
await p.save();
count++;
} catch (e) {
console.error("Update error:", e.message);
}
}
await ctx.reply(i18n.admin.inv_updated.replace('{count}', count), { parse_mode: 'Markdown', ...Markup.removeKeyboard() });
return ctx.scene.leave();
}
// --- Excel Import Logic ---
if (ctx.wizard.state.action === 'excel_import') {
if (!ctx.message.document) {
await ctx.reply(i18n.admin.error || "Error");
return;
}
const doc = ctx.message.document;
if (!doc.file_name.endsWith('.xlsx')) {
await ctx.reply("Only .xlsx");
return;
}
await ctx.reply(i18n.admin.broadcast_sending || "⏳...");
try {
const fileLink = await ctx.telegram.getFileLink(doc.file_id);
const response = await axios({
url: fileLink.href,
method: 'GET',
responseType: 'arraybuffer'
});
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(response.data);
const worksheet = workbook.getWorksheet(1); // First sheet
let imported = 0;
let updated = 0;
// Iterate rows (skip header)
worksheet.eachRow(async (row, rowNumber) => {
if (rowNumber === 1) return; // Header
const id = row.getCell(1).value;
const name = row.getCell(2).value;
const price = row.getCell(3).value;
const category = row.getCell(4).value; // logic: text or id? let's assume raw text/id
const quantity = row.getCell(5).value;
const description = row.getCell(6).value;
if (id && name && price) {
const existing = await Product.findOne({ id: id });
if (existing) {
existing.name = name;
existing.price = price;
existing.category = category;
existing.quantity = quantity || 0;
existing.description = description || "";
await existing.save();
updated++;
} else {
const newProd = new Product({
id: id,
name: name,
price: price,
category: category,
quantity: quantity || 0,
description: description || ""
});
await newProd.save();
imported++;
}
}
});
await ctx.reply(`${i18n.admin.inv_import_success}\n🆕: ~${imported}\n🔄: ~${updated}`, { parse_mode: 'Markdown', ...Markup.removeKeyboard() });
return ctx.scene.leave();
} catch (err) {
console.error(err);
await ctx.reply(i18n.admin.error);
return ctx.scene.leave();
}
}
}
);
module.exports = inventoryScene;