Merge pull request 'feature/admin-section' (#20) from feature/admin-section into main

Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
1323ed5
2024-11-15 12:34:09 +00:00
4 changed files with 813 additions and 569 deletions

BIN
corrupt-photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -1,6 +1,7 @@
import db from '../config/database.js'; import db from '../config/database.js';
import config from '../config/config.js'; import config from '../config/config.js';
import fs from 'fs/promises'; import fs from 'fs/promises';
import User from "../models/User.js";
export default class AdminProductHandler { export default class AdminProductHandler {
constructor(bot) { constructor(bot) {
@@ -198,7 +199,10 @@ export default class AdminProductHandler {
{ {
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '« Back to Categories', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` } {
text: '« Back to Categories',
callback_data: `prod_district_${location.country}_${location.city}_${location.district}`
}
]] ]]
} }
} }
@@ -259,7 +263,10 @@ export default class AdminProductHandler {
}]), }]),
[{text: ' Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}`}], [{text: ' Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}`}],
[{text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}`}], [{text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}`}],
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] [{
text: '« Back',
callback_data: `prod_district_${location.country}_${location.city}_${location.district}`
}]
] ]
}; };
@@ -297,7 +304,10 @@ export default class AdminProductHandler {
{ {
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '« Back to Subcategories', callback_data: `prod_category_${locationId}_${categoryId}` } {
text: '« Back to Subcategories',
callback_data: `prod_category_${locationId}_${categoryId}`
}
]] ]]
} }
} }
@@ -336,20 +346,40 @@ export default class AdminProductHandler {
); );
} }
async handleSubcategorySelection(callbackQuery) { async viewProductsPage(locationId, categoryId, subcategoryId, page) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_');
try { try {
const limit = 10;
const offset = (page || 0) * limit;
const previousPage = page > 0 ? page - 1 : 0;
const nextPage = page + 1;
const products = await db.allAsync( const products = await db.allAsync(
`SELECT id, name, price, quantity_in_stock `SELECT id, name, price, quantity_in_stock
FROM products FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ? WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
ORDER BY name`, ORDER BY name
[locationId, categoryId, subcategoryId] LIMIT ?
OFFSET ?
`,
[locationId, categoryId, subcategoryId, limit, offset]
); );
if ((products.length === 0) && (page == 0)) {
return {
text: 'No products for this location',
markup: {
inline_keyboard: [
[{text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}`}]
]
}
};
}
if ((products.length === 0) && (page > 0)) {
return await this.viewProductsPage(locationId, categoryId, subcategoryId, previousPage);
}
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]); const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]); const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
@@ -359,17 +389,48 @@ export default class AdminProductHandler {
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`, text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
callback_data: `view_product_${prod.id}` callback_data: `view_product_${prod.id}`
}]), }]),
[{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}` }], [{
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }] text: '📥 Import Products',
callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}`
}],
] ]
}; };
keyboard.inline_keyboard.push([
{text: `«`, callback_data: `list_products_${locationId}_${categoryId}_${subcategoryId}_${previousPage}`},
{text: `»`, callback_data: `list_products_${locationId}_${categoryId}_${subcategoryId}_${nextPage}`},
]);
keyboard.inline_keyboard.push([
{text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}`}
]);
return {
text: `📦 ${category.name} > ${subcategory.name}\nSelect product or import new ones:`,
markup: keyboard
}
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
return {text: 'Error loading products. Please try again.'};
}
}
async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_');
try {
const {text, markup} = await this.viewProductsPage(locationId, categoryId, subcategoryId, 0);
await this.bot.editMessageText( await this.bot.editMessageText(
`📦 ${category.name} > ${subcategory.name}\nSelect product or import new ones:`, text,
{ {
chat_id: chatId, chat_id: chatId,
message_id: messageId, message_id: messageId,
reply_markup: keyboard reply_markup: markup
} }
); );
} catch (error) { } catch (error) {
@@ -378,6 +439,28 @@ export default class AdminProductHandler {
} }
} }
async handleProductListPage(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) {
return;
}
const chatId = callbackQuery.message.chat.id;
const [locationId, categoryId, subcategoryId, page] = callbackQuery.data.replace('list_products_', '').split("_");
try {
const {text, markup} = await this.viewProductsPage(locationId, categoryId, subcategoryId, parseInt(page));
await this.bot.editMessageText(text, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: markup,
parse_mode: 'HTML'
});
} catch (e) {
return;
}
}
async handleAddProduct(callbackQuery) { async handleAddProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
@@ -421,7 +504,10 @@ export default class AdminProductHandler {
parse_mode: 'HTML', parse_mode: 'HTML',
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` } {
text: '❌ Cancel',
callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}`
}
]] ]]
} }
}); });
@@ -449,8 +535,11 @@ export default class AdminProductHandler {
} }
const file = await this.bot.getFile(msg.document.file_id); const file = await this.bot.getFile(msg.document.file_id);
const fileContent = await this.bot.downloadFile(file.file_id, '/tmp');
const fileContent = await this.bot.downloadFile(file.file_id, '.');
jsonContent = await fs.readFile(fileContent, 'utf8'); jsonContent = await fs.readFile(fileContent, 'utf8');
await fs.rm(fileContent);
} else if (msg.text) { } else if (msg.text) {
jsonContent = msg.text; jsonContent = msg.text;
} else { } else {
@@ -495,7 +584,10 @@ export default class AdminProductHandler {
{ {
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '« Back to Products', callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` } {
text: '« Back to Products',
callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}`
}
]] ]]
} }
} }
@@ -503,9 +595,9 @@ export default class AdminProductHandler {
this.userStates.delete(chatId); this.userStates.delete(chatId);
} catch (error) { } catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error importing products:', error); console.error('Error importing products:', error);
await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
await db.runAsync('ROLLBACK');
} }
return true; return true;
@@ -555,26 +647,137 @@ Coordinates: ${product.hidden_coordinates}
{text: '✏️ Edit', callback_data: `edit_product_${productId}`}, {text: '✏️ Edit', callback_data: `edit_product_${productId}`},
{text: '❌ Delete', callback_data: `delete_product_${productId}`} {text: '❌ Delete', callback_data: `delete_product_${productId}`}
], ],
[{ text: '« Back', callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}` }] [{
text: '« Back',
callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}`
}]
] ]
}; };
// Send product photos // Send product photos
if (product.photo_url) { if (product.photo_url) {
try {
await this.bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'}); await this.bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
} catch (e) {
await this.bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
}
} }
if (product.hidden_photo_url) { if (product.hidden_photo_url) {
try {
await this.bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'}); await this.bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
} catch (e) {
await this.bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
}
} }
await this.bot.editMessageText(message, { await this.bot.deleteMessage(chatId, messageId);
chat_id: chatId, await this.bot.sendMessage(chatId, message, {reply_markup: keyboard});
message_id: messageId,
reply_markup: keyboard
});
} catch (error) { } catch (error) {
console.error('Error in handleViewProduct:', error); console.error('Error in handleViewProduct:', error);
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.'); await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
} }
} }
async handleProductDelete(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const productId = callbackQuery.data.replace('delete_product_', '');
const chatId = callbackQuery.message.chat.id;
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name,
l.country, l.city, l.district
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
JOIN locations l ON p.location_id = l.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const keyboard = {
inline_keyboard: [
[
{text: '✅ Confirm Delete', callback_data: `confirm_delete_product_${productId}`},
{text: '❌ Cancel', callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}`}
]
]
};
await this.bot.editMessageText(
`⚠️ Are you sure you want to delete product\n\nThis action cannot be undone!`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
}
);
} catch (error) {
console.error('Error in handleDeleteUser:', error);
await this.bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
}
}
async handleConfirmDelete(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const productId = callbackQuery.data.replace('confirm_delete_product_', '');
const chatId = callbackQuery.message.chat.id;
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name,
l.country, l.city, l.district
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
JOIN locations l ON p.location_id = l.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
try {
await db.runAsync('BEGIN TRANSACTION');
await db.runAsync('DELETE FROM products WHERE id=?', [productId.toString()]);
await db.runAsync('COMMIT');
} catch (e) {
await db.runAsync("ROLLBACK");
console.error('Error deleting product:', error);
throw error;
}
const keyboard = {
inline_keyboard: [
[{
text: '« Back',
callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}`
}]
]
};
await this.bot.editMessageText(
`✅ Product has been successfully deleted.`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleConfirmDelete:', error);
await this.bot.sendMessage(chatId, 'Error deleting product. Please try again.');
}
}
} }

View File

@@ -1,11 +1,35 @@
import db from '../config/database.js';
import User from '../models/User.js'; import User from '../models/User.js';
import config from "../config/config.js";
export default class UserHandler { export default class UserHandler {
constructor(bot) { constructor(bot) {
this.bot = bot; this.bot = bot;
} }
async canUseBot(msg) {
const userId = msg.from.id;
const user = await User.getById(userId);
const keyboard = {
inline_keyboard: [
[{text: "Contact support", url: config.SUPPORT_LINK}]
]
};
switch (user?.status) {
case 0:
return true;
case 1:
await this.bot.sendMessage(userId, '⚠Your account has been deleted by administrator', {reply_markup: keyboard});
return false;
case 2:
await this.bot.sendMessage(userId, '⚠Your account has been blocked by administrator', {reply_markup: keyboard});
return false;
default:
return true
}
}
async showProfile(msg) { async showProfile(msg) {
const chatId = msg.chat.id; const chatId = msg.chat.id;
const userId = msg.from.id; const userId = msg.from.id;

View File

@@ -69,7 +69,15 @@ bot.onText(/\/admin/, async (msg) => {
// Handle user menu buttons // Handle user menu buttons
bot.on('message', async (msg) => { bot.on('message', async (msg) => {
if (!msg.text) return; if (msg.text && msg.text.toLowerCase() === '/start') {
return;
}
const canUse = await userHandler.canUseBot(msg);
if (!canUse) {
return;
}
if (msg.text.toLowerCase() === '/start') { if (msg.text.toLowerCase() === '/start') {
return; return;
@@ -271,12 +279,21 @@ bot.on('callback_query', async (callbackQuery) => {
} else if (action.startsWith('prod_subcategory_')) { } else if (action.startsWith('prod_subcategory_')) {
logDebug(action, 'handleSubcategorySelection'); logDebug(action, 'handleSubcategorySelection');
await adminProductHandler.handleSubcategorySelection(callbackQuery); await adminProductHandler.handleSubcategorySelection(callbackQuery);
} else if (action.startsWith('list_products_')) {
logDebug(action, 'handleSubcategorySelection');
await adminProductHandler.handleProductListPage(callbackQuery);
} else if (action.startsWith('add_product_')) { } else if (action.startsWith('add_product_')) {
logDebug(action, 'handleAddProduct'); logDebug(action, 'handleAddProduct');
await adminProductHandler.handleAddProduct(callbackQuery); await adminProductHandler.handleAddProduct(callbackQuery);
} else if (action.startsWith('view_product_')) { } else if (action.startsWith('view_product_')) {
logDebug(action, 'handleViewProduct'); logDebug(action, 'handleViewProduct');
await adminProductHandler.handleViewProduct(callbackQuery); await adminProductHandler.handleViewProduct(callbackQuery);
} else if (action.startsWith('delete_product_')) {
logDebug(action, 'handleViewProduct');
await adminProductHandler.handleProductDelete(callbackQuery);
} else if (action.startsWith('confirm_delete_product_')) {
logDebug(action, 'handleConfirmDelete');
await adminProductHandler.handleConfirmDelete(callbackQuery);
} }
// Admin user management // Admin user management
else if (action.startsWith('view_user_')) { else if (action.startsWith('view_user_')) {