Spaces:
Paused
Paused
Deploy Bot commited on
Commit ·
06e2bc3
1
Parent(s): 1e66a57
Feat: Add Product Condition (New vs Used)
Browse files- src/controllers/userController.js +28 -9
- src/main.js +22 -6
- src/models/Product.js +1 -0
- src/scenes/admin/addProduct.js +32 -1
src/controllers/userController.js
CHANGED
|
@@ -189,21 +189,39 @@ exports.showSubCategories = async (ctx, parentId) => {
|
|
| 189 |
}
|
| 190 |
};
|
| 191 |
|
| 192 |
-
// Show Products
|
| 193 |
exports.showProducts = async (ctx, catId) => {
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
};
|
| 197 |
|
| 198 |
// Browse Category (Carousel Logic)
|
| 199 |
-
exports.browseCategory = async (ctx, catId, prodIndex, mediaIndex = 0) => {
|
| 200 |
try {
|
| 201 |
const id = catId.replace('cat_', '');
|
| 202 |
const category = await Category.findOne({ id: id });
|
| 203 |
if (!category) return ctx.reply("Kategoriya topilmadi.");
|
| 204 |
|
| 205 |
-
|
| 206 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
// Handle Product Index Bounds
|
| 209 |
let page = parseInt(prodIndex);
|
|
@@ -238,15 +256,16 @@ exports.browseCategory = async (ctx, catId, prodIndex, mediaIndex = 0) => {
|
|
| 238 |
|
| 239 |
// Buttons
|
| 240 |
// Navigation: prev/next product. Media index resets to 0.
|
|
|
|
| 241 |
let navigationRow = [
|
| 242 |
-
Markup.button.callback("⬅️", `br_${id}_${page - 1}
|
| 243 |
Markup.button.callback(`${page + 1} / ${products.length}`, "noop"),
|
| 244 |
-
Markup.button.callback("➡️", `br_${id}_${page + 1}
|
| 245 |
];
|
| 246 |
|
| 247 |
let mediaRow = [];
|
| 248 |
if (product.media && product.media.length > 1) {
|
| 249 |
-
mediaRow.push(Markup.button.callback(`📸 ${mPage + 1}/${product.media.length}`, `br_${id}_${page}_${mPage + 1}`));
|
| 250 |
}
|
| 251 |
|
| 252 |
let actionRow = [];
|
|
|
|
| 189 |
}
|
| 190 |
};
|
| 191 |
|
| 192 |
+
// Show Products (Ask for Condition first)
|
| 193 |
exports.showProducts = async (ctx, catId) => {
|
| 194 |
+
const id = catId.replace('cat_', '');
|
| 195 |
+
const buttons = [
|
| 196 |
+
[
|
| 197 |
+
Markup.button.callback("🆕 Yangi", `cond_${id}_new`),
|
| 198 |
+
Markup.button.callback("♻️ B/U", `cond_${id}_used`)
|
| 199 |
+
],
|
| 200 |
+
[Markup.button.callback("🌐 Barchasi", `cond_${id}_all`)],
|
| 201 |
+
[Markup.button.callback(ctx.i18n.btn_back_nav || "⬅️ Orqaga", "back_to_cats")]
|
| 202 |
+
];
|
| 203 |
+
// Try edit, fallback to reply
|
| 204 |
+
await ctx.editMessageText("🔎 Qaysi turdagi mahsulotlarni ko'rmoqchisiz?", Markup.inlineKeyboard(buttons))
|
| 205 |
+
.catch(() => ctx.reply("🔎 Qaysi turdagi mahsulotlarni ko'rmoqchisiz?", Markup.inlineKeyboard(buttons)));
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
// Filter Handler
|
| 209 |
+
exports.filterByCondition = async (ctx, catId, condition) => {
|
| 210 |
+
return exports.browseCategory(ctx, catId, 0, 0, condition);
|
| 211 |
};
|
| 212 |
|
| 213 |
// Browse Category (Carousel Logic)
|
| 214 |
+
exports.browseCategory = async (ctx, catId, prodIndex, mediaIndex = 0, conditionFilter = 'all') => {
|
| 215 |
try {
|
| 216 |
const id = catId.replace('cat_', '');
|
| 217 |
const category = await Category.findOne({ id: id });
|
| 218 |
if (!category) return ctx.reply("Kategoriya topilmadi.");
|
| 219 |
|
| 220 |
+
let query = { category: category.name };
|
| 221 |
+
if (conditionFilter !== 'all') query.condition = conditionFilter;
|
| 222 |
+
|
| 223 |
+
const products = await Product.find(query);
|
| 224 |
+
if (!products || products.length === 0) return ctx.reply("❌ Bu kategoriyada mahsulotlar yo'q.");
|
| 225 |
|
| 226 |
// Handle Product Index Bounds
|
| 227 |
let page = parseInt(prodIndex);
|
|
|
|
| 256 |
|
| 257 |
// Buttons
|
| 258 |
// Navigation: prev/next product. Media index resets to 0.
|
| 259 |
+
// Format: br_catId_prodIndex_mediaIndex_condition
|
| 260 |
let navigationRow = [
|
| 261 |
+
Markup.button.callback("⬅️", `br_${id}_${page - 1}_0_${conditionFilter}`),
|
| 262 |
Markup.button.callback(`${page + 1} / ${products.length}`, "noop"),
|
| 263 |
+
Markup.button.callback("➡️", `br_${id}_${page + 1}_0_${conditionFilter}`)
|
| 264 |
];
|
| 265 |
|
| 266 |
let mediaRow = [];
|
| 267 |
if (product.media && product.media.length > 1) {
|
| 268 |
+
mediaRow.push(Markup.button.callback(`📸 ${mPage + 1}/${product.media.length}`, `br_${id}_${page}_${mPage + 1}_${conditionFilter}`));
|
| 269 |
}
|
| 270 |
|
| 271 |
let actionRow = [];
|
src/main.js
CHANGED
|
@@ -450,13 +450,29 @@ bot.action(/cat_(.+)/, async (ctx) => {
|
|
| 450 |
});
|
| 451 |
|
| 452 |
// Carousel Navigation
|
| 453 |
-
//
|
| 454 |
-
bot.action(/
|
| 455 |
-
// pattern: br_catId_prodIndex_mediaIndex
|
| 456 |
const catId = "cat_" + ctx.match[1];
|
| 457 |
-
const
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
});
|
| 461 |
|
| 462 |
// Backward compatibility or redirect
|
|
|
|
| 450 |
});
|
| 451 |
|
| 452 |
// Carousel Navigation
|
| 453 |
+
// Condition Filter Selection
|
| 454 |
+
bot.action(/cond_(.+)_(.+)/, (ctx) => {
|
|
|
|
| 455 |
const catId = "cat_" + ctx.match[1];
|
| 456 |
+
const condition = ctx.match[2];
|
| 457 |
+
userController.filterByCondition(ctx, catId, condition);
|
| 458 |
+
});
|
| 459 |
+
|
| 460 |
+
// Carousel Navigation (New)
|
| 461 |
+
bot.action(/br_(.+)/, (ctx) => {
|
| 462 |
+
// pattern: br_catId_prodIndex_mediaIndex_condition
|
| 463 |
+
// We split manually because condition is optional and captured groups are tricky
|
| 464 |
+
const text = ctx.match[1];
|
| 465 |
+
const parts = text.split('_');
|
| 466 |
+
|
| 467 |
+
// Safety check
|
| 468 |
+
if (parts.length < 3) return ctx.answerCbQuery("Error");
|
| 469 |
+
|
| 470 |
+
const catId = "cat_" + parts[0];
|
| 471 |
+
const prodIndex = parseInt(parts[1]);
|
| 472 |
+
const mediaIndex = parseInt(parts[2]);
|
| 473 |
+
const condition = parts[3] || 'all'; // Default to all if missing
|
| 474 |
+
|
| 475 |
+
userController.browseCategory(ctx, catId, prodIndex, mediaIndex, condition);
|
| 476 |
});
|
| 477 |
|
| 478 |
// Backward compatibility or redirect
|
src/models/Product.js
CHANGED
|
@@ -7,6 +7,7 @@ const productSchema = new mongoose.Schema({
|
|
| 7 |
originalPrice: { type: Number, default: null }, // For Flash Sales
|
| 8 |
discountPercent: { type: Number, default: 0 }, // Discount %
|
| 9 |
quantity: { type: Number, default: 0 },
|
|
|
|
| 10 |
description: String,
|
| 11 |
category: String, // Category ID
|
| 12 |
media: [{
|
|
|
|
| 7 |
originalPrice: { type: Number, default: null }, // For Flash Sales
|
| 8 |
discountPercent: { type: Number, default: 0 }, // Discount %
|
| 9 |
quantity: { type: Number, default: 0 },
|
| 10 |
+
condition: { type: String, enum: ['new', 'used'], default: 'new' },
|
| 11 |
description: String,
|
| 12 |
category: String, // Category ID
|
| 13 |
media: [{
|
src/scenes/admin/addProduct.js
CHANGED
|
@@ -64,10 +64,41 @@ const addProductScene = new Scenes.WizardScene(
|
|
| 64 |
if (!ctx.message || !ctx.message.text || isNaN(ctx.message.text)) return ctx.reply(i18n.admin.error_num);
|
| 65 |
ctx.wizard.state.product.quantity = parseInt(ctx.message.text);
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
ctx.reply(i18n.admin.enter_desc, Markup.keyboard([[i18n.btn_cancel]]).resize());
|
| 68 |
return ctx.wizard.next();
|
| 69 |
},
|
| 70 |
-
// Step
|
| 71 |
async (ctx) => {
|
| 72 |
const i18n = ctx.i18n;
|
| 73 |
if (ctx.message && ctx.message.text === i18n.btn_cancel) {
|
|
|
|
| 64 |
if (!ctx.message || !ctx.message.text || isNaN(ctx.message.text)) return ctx.reply(i18n.admin.error_num);
|
| 65 |
ctx.wizard.state.product.quantity = parseInt(ctx.message.text);
|
| 66 |
|
| 67 |
+
// ASK CONDITION
|
| 68 |
+
ctx.reply("Maxsulot xolati qanday?", Markup.keyboard([
|
| 69 |
+
["🆕 Yangi", "♻️ B/U"],
|
| 70 |
+
[i18n.btn_cancel]
|
| 71 |
+
]).resize());
|
| 72 |
+
return ctx.wizard.next();
|
| 73 |
+
},
|
| 74 |
+
// Step 5: Handle Condition & Ask Description
|
| 75 |
+
(ctx) => {
|
| 76 |
+
const i18n = ctx.i18n;
|
| 77 |
+
const text = ctx.message.text;
|
| 78 |
+
if (text === i18n.btn_cancel) {
|
| 79 |
+
ctx.scene.leave();
|
| 80 |
+
userController.start(ctx, i18n.cancel_process);
|
| 81 |
+
return;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if (text === "🆕 Yangi") {
|
| 85 |
+
ctx.wizard.state.product.condition = 'new';
|
| 86 |
+
} else if (text === "♻️ B/U") {
|
| 87 |
+
ctx.wizard.state.product.condition = 'used';
|
| 88 |
+
} else {
|
| 89 |
+
// Default or Retry? Let's assume buttons are clicked.
|
| 90 |
+
// If manual type, maybe fallback to 'new' or ask again.
|
| 91 |
+
// Stricter:
|
| 92 |
+
if (text !== "🆕 Yangi" && text !== "♻️ B/U") {
|
| 93 |
+
return ctx.reply("Iltimos tugmani tanlang: Yangi yoki B/U");
|
| 94 |
+
}
|
| 95 |
+
ctx.wizard.state.product.condition = text === "🆕 Yangi" ? 'new' : 'used';
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
ctx.reply(i18n.admin.enter_desc, Markup.keyboard([[i18n.btn_cancel]]).resize());
|
| 99 |
return ctx.wizard.next();
|
| 100 |
},
|
| 101 |
+
// Step 6: Ask Category (Shifted)
|
| 102 |
async (ctx) => {
|
| 103 |
const i18n = ctx.i18n;
|
| 104 |
if (ctx.message && ctx.message.text === i18n.btn_cancel) {
|