From 4251f1a0bd7e14997358258a5b60e7cde16e8675 Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Fri, 15 Nov 2024 02:26:13 +0300 Subject: [PATCH 1/5] user deletion/blocking --- src/config/database.js | 1 + src/handlers/adminLocationHandler.js | 4 +- src/handlers/adminUserHandler.js | 74 ++++++++++++- src/handlers/userHandler.js | 158 +++++++++++++++------------ src/index.js | 32 +++++- src/models/User.js | 120 +++++++++++++------- 6 files changed, 278 insertions(+), 111 deletions(-) diff --git a/src/config/database.js b/src/config/database.js index e7a2e22..33c0ff6 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -94,6 +94,7 @@ const initDb = async () => { country TEXT, city TEXT, district TEXT, + status INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); diff --git a/src/handlers/adminLocationHandler.js b/src/handlers/adminLocationHandler.js index e23233b..7c05143 100644 --- a/src/handlers/adminLocationHandler.js +++ b/src/handlers/adminLocationHandler.js @@ -215,7 +215,7 @@ export default class AdminLocationHandler { const keyboard = { inline_keyboard: locations.map(loc => [{ text: `${loc.country} > ${loc.city} > ${loc.district} (P:${loc.product_count} C:${loc.category_count})`, - callback_data: `confirm_delete_${loc.country}_${loc.city}_${loc.district}` + callback_data: `confirm_delete_location_${loc.country}_${loc.city}_${loc.district}` }]) }; @@ -239,7 +239,7 @@ export default class AdminLocationHandler { async handleConfirmDelete(callbackQuery) { const chatId = callbackQuery.message.chat.id; const [country, city, district] = callbackQuery.data - .replace('confirm_delete_', '') + .replace('confirm_delete_location_', '') .split('_'); try { diff --git a/src/handlers/adminUserHandler.js b/src/handlers/adminUserHandler.js index 25717ef..7ee340d 100644 --- a/src/handlers/adminUserHandler.js +++ b/src/handlers/adminUserHandler.js @@ -265,7 +265,7 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price} const chatId = callbackQuery.message.chat.id; try { - await User.delete(userId); + await User.updateUserStatus(userId, 1); const keyboard = { inline_keyboard: [ @@ -273,6 +273,12 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price} ] }; + try { + await this.bot.sendMessage(userId, '⚠️Your account has been deleted by administrator'); + } catch (e) { + // ignore if we can't notify user + } + await this.bot.editMessageText( `✅ User ${userId} has been successfully deleted.`, { @@ -287,6 +293,72 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price} } } + async handleBlockUser(callbackQuery) { + if (!this.isAdmin(callbackQuery.from.id)) return; + + const userId = callbackQuery.data.replace('block_user_', ''); + const chatId = callbackQuery.message.chat.id; + + try { + const keyboard = { + inline_keyboard: [ + [ + {text: '✅ Confirm Block', callback_data: `confirm_block_user_${userId}`}, + {text: '❌ Cancel', callback_data: `view_user_${userId}`} + ] + ] + }; + + await this.bot.editMessageText( + `⚠️ Are you sure you want to block user ${userId}?`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: keyboard, + parse_mode: 'HTML' + } + ); + } catch (error) { + console.error('Error in handleBlockUser:', error); + await this.bot.sendMessage(chatId, 'Error processing block request. Please try again.'); + } + } + + async handleConfirmBlock(callbackQuery) { + if (!this.isAdmin(callbackQuery.from.id)) return; + + const userId = callbackQuery.data.replace('confirm_block_user_', ''); + const chatId = callbackQuery.message.chat.id; + + try { + await User.updateUserStatus(userId, 2); + + const keyboard = { + inline_keyboard: [ + [{text: '« Back to User List', callback_data: 'admin_users'}] + ] + }; + + try { + await this.bot.sendMessage(userId, '⚠️Your account has been blocked by administrator'); + } catch (e) { + // ignore if we can't notify user + } + + await this.bot.editMessageText( + `✅ User ${userId} has been successfully blocked.`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleConfirmBlock:', error); + await this.bot.sendMessage(chatId, 'Error blocking user. Please try again.'); + } + } + async handleEditUserBalance(callbackQuery) { if (!this.isAdmin(callbackQuery.from.id)) return; diff --git a/src/handlers/userHandler.js b/src/handlers/userHandler.js index 6aa4b49..3dcac62 100644 --- a/src/handlers/userHandler.js +++ b/src/handlers/userHandler.js @@ -1,28 +1,52 @@ -import db from '../config/database.js'; import User from '../models/User.js'; +import config from "../config/config.js"; export default class UserHandler { - constructor(bot) { - this.bot = bot; - } + constructor(bot) { + this.bot = bot; + } - async showProfile(msg) { - const chatId = msg.chat.id; - const userId = msg.from.id; + async canUseBot(msg) { + const userId = msg.from.id; + const user = await User.getById(userId); - try { - const userStats = await User.getUserStats(userId); + const keyboard = { + inline_keyboard: [ + [{text: "Contact support", url: config.SUPPORT_LINK}] + ] + }; - if (!userStats) { - await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); - return; - } + 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 + } + } - const locationText = userStats.country && userStats.city && userStats.district - ? `${userStats.country}, ${userStats.city}, ${userStats.district}` - : 'Not set'; + async showProfile(msg) { + const chatId = msg.chat.id; + const userId = msg.from.id; - const text = ` + try { + const userStats = await User.getUserStats(userId); + + if (!userStats) { + await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); + return; + } + + const locationText = userStats.country && userStats.city && userStats.district + ? `${userStats.country}, ${userStats.city}, ${userStats.district}` + : 'Not set'; + + const text = ` 👤 *Your Profile* 📱 Telegram ID: \`${userId}\` @@ -37,58 +61,58 @@ export default class UserHandler { 📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()} `; - const keyboard = { - inline_keyboard: [ - [{ text: '📍 Set Location', callback_data: 'set_location' }], - [{ text: '❌ Delete Account', callback_data: 'delete_account' }] - ] - }; + const keyboard = { + inline_keyboard: [ + [{text: '📍 Set Location', callback_data: 'set_location'}], + [{text: '❌ Delete Account', callback_data: 'delete_account'}] + ] + }; - await this.bot.sendMessage(chatId, text, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); - } catch (error) { - console.error('Error in showProfile:', error); - await this.bot.sendMessage(chatId, 'Error loading profile. Please try again.'); - } - } - - async handleStart(msg) { - const chatId = msg.chat.id; - const userId = msg.from.id; - const username = msg.chat.username; - - try { - // Create user profile - await User.create(userId, username); - - const keyboard = { - reply_markup: { - keyboard: [ - ['📦 Products', '👤 Profile'], - ['🛍 Purchases', '💰 Wallets'] - ], - resize_keyboard: true + await this.bot.sendMessage(chatId, text, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + } catch (error) { + console.error('Error in showProfile:', error); + await this.bot.sendMessage(chatId, 'Error loading profile. Please try again.'); } - }; - - await this.bot.sendMessage( - chatId, - 'Welcome to the shop! Choose an option:', - keyboard - ); - } catch (error) { - console.error('Error in handleStart:', error); - await this.bot.sendMessage(chatId, 'Error creating user profile. Please try again.'); } - } - async handleBackToProfile(callbackQuery) { - await this.showProfile({ - chat: { id: callbackQuery.message.chat.id }, - from: { id: callbackQuery.from.id } - }); - await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id); - } + async handleStart(msg) { + const chatId = msg.chat.id; + const userId = msg.from.id; + const username = msg.chat.username; + + try { + // Create user profile + await User.create(userId, username); + + const keyboard = { + reply_markup: { + keyboard: [ + ['📦 Products', '👤 Profile'], + ['🛍 Purchases', '💰 Wallets'] + ], + resize_keyboard: true + } + }; + + await this.bot.sendMessage( + chatId, + 'Welcome to the shop! Choose an option:', + keyboard + ); + } catch (error) { + console.error('Error in handleStart:', error); + await this.bot.sendMessage(chatId, 'Error creating user profile. Please try again.'); + } + } + + async handleBackToProfile(callbackQuery) { + await this.showProfile({ + chat: {id: callbackQuery.message.chat.id}, + from: {id: callbackQuery.from.id} + }); + await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id); + } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index fde65c1..1f832eb 100644 --- a/src/index.js +++ b/src/index.js @@ -43,6 +43,13 @@ const adminProductHandler = new AdminProductHandler(bot); // Start command - Create user profile bot.onText(/\/start/, async (msg) => { logDebug('/start', 'handleStart'); + + const canUse = await userHandler.canUseBot(msg); + + if (!canUse) { + return; + } + try { await userHandler.handleStart(msg); } catch (error) { @@ -64,6 +71,16 @@ bot.onText(/\/admin/, async (msg) => { bot.on('message', async (msg) => { if (!msg.text) return; + if (msg.text.toLowerCase() === '/start') { + return; + } + + const canUse = await userHandler.canUseBot(msg); + + if (!canUse) { + return; + } + try { // Check for admin location input if (await adminLocationHandler.handleLocationInput(msg)) { @@ -126,6 +143,13 @@ bot.on('callback_query', async (callbackQuery) => { const action = callbackQuery.data; const msg = callbackQuery.message; + const canUse = await userHandler.canUseBot(callbackQuery); + + if (!canUse) { + await bot.answerCallbackQuery(callbackQuery.id); + return; + } + try { // Profile and location management if (action === 'set_location') { @@ -211,7 +235,7 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action === 'delete_location') { logDebug(action, 'handleDeleteLocation'); await adminLocationHandler.handleDeleteLocation(callbackQuery); - } else if (action.startsWith('confirm_delete_')) { + } else if (action.startsWith('confirm_delete_location_')) { logDebug(action, 'handleConfirmDelete'); await adminLocationHandler.handleConfirmDelete(callbackQuery); } else if (action === 'admin_menu') { @@ -264,9 +288,15 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action.startsWith('delete_user_')) { logDebug(action, 'handleDeleteUser'); await adminUserHandler.handleDeleteUser(callbackQuery); + } else if (action.startsWith('block_user_')) { + logDebug(action, 'handleDeleteUser'); + await adminUserHandler.handleBlockUser(callbackQuery); } else if (action.startsWith('confirm_delete_user_')) { logDebug(action, 'handleConfirmDelete'); await adminUserHandler.handleConfirmDelete(callbackQuery); + } else if (action.startsWith('confirm_block_user_')) { + logDebug(action, 'handleConfirmBlock'); + await adminUserHandler.handleConfirmBlock(callbackQuery); } else if (action.startsWith('edit_user_balance_')) { logDebug(action, 'handleEditUserBalance'); await adminUserHandler.handleEditUserBalance(callbackQuery); diff --git a/src/models/User.js b/src/models/User.js index 9e1de17..75e3bf7 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -1,50 +1,50 @@ import db from '../config/database.js'; export default class User { - static async create(telegramId, username) { - try { - // First check if user exists - const existingUser = await this.getById(telegramId); - if (existingUser) { - return existingUser.id; - } + static async create(telegramId, username) { + try { + // First check if user exists + const existingUser = await this.getById(telegramId); + if (existingUser) { + return existingUser.id; + } - // Begin transaction - await db.runAsync('BEGIN TRANSACTION'); + // Begin transaction + await db.runAsync('BEGIN TRANSACTION'); - // Create new user - const result = await db.runAsync( - 'INSERT INTO users (telegram_id, username) VALUES (?, ?)', - [telegramId.toString(), username] - ); + // Create new user + const result = await db.runAsync( + 'INSERT INTO users (telegram_id, username) VALUES (?, ?)', + [telegramId.toString(), username] + ); - // Commit transaction - await db.runAsync('COMMIT'); + // Commit transaction + await db.runAsync('COMMIT'); - return result.lastID; - } catch (error) { - // Rollback on error - await db.runAsync('ROLLBACK'); - console.error('Error creating user:', error); - throw error; + return result.lastID; + } catch (error) { + // Rollback on error + await db.runAsync('ROLLBACK'); + console.error('Error creating user:', error); + throw error; + } } - } - static async getById(telegramId) { - try { - return await db.getAsync( - 'SELECT * FROM users WHERE telegram_id = ?', - [telegramId.toString()] - ); - } catch (error) { - console.error('Error getting user:', error); - throw error; + static async getById(telegramId) { + try { + return await db.getAsync( + 'SELECT * FROM users WHERE telegram_id = ?', + [telegramId.toString()] + ); + } catch (error) { + console.error('Error getting user:', error); + throw error; + } } - } - static async getUserStats(telegramId) { - try { - return await db.getAsync(` + static async getUserStats(telegramId) { + try { + return await db.getAsync(` SELECT u.*, COUNT(DISTINCT p.id) as purchase_count, @@ -58,9 +58,49 @@ export default class User { WHERE u.telegram_id = ? GROUP BY u.id `, [telegramId.toString()]); - } catch (error) { - console.error('Error getting user stats:', error); - throw error; + } catch (error) { + console.error('Error getting user stats:', error); + throw error; + } + } + + static async updateUserStatus(telegramId, status) { + // statuses + // 0 - active + // 1 - deleted + // 2 - blocked + + try { + await db.runAsync('BEGIN TRANSACTION'); + + // Update user status + await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, telegramId.toString()]); + + // Commit transaction + await db.runAsync('COMMIT'); + } catch (e) { + await db.runAsync("ROLLBACK"); + console.error('Error deleting user:', error); + throw error; + } + } + + static async delete(telegramId) { + try { + await db.runAsync('BEGIN TRANSACTION'); + + // Delete user and his data + await db.runAsync('DELETE FROM users WHERE telegram_id = ?', [telegramId.toString()]); + await db.runAsync('DELETE FROM transactions WHERE user_id = ?', [telegramId.toString()]); + await db.runAsync('DELETE FROM purchases WHERE user_id = ?', [telegramId.toString()]); + await db.runAsync('DELETE FROM crypto_wallets WHERE user_id = ?', [telegramId.toString()]); + + // Commit transaction + await db.runAsync('COMMIT'); + } catch (e) { + await db.runAsync("ROLLBACK"); + console.error('Error deleting user:', error); + throw error; + } } - } } \ No newline at end of file From de5e4050934678b54394fc710ff9aa23c99139ca Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Fri, 15 Nov 2024 07:07:02 +0300 Subject: [PATCH 2/5] import from json --- src/handlers/adminProductHandler.js | 7 +++++-- src/index.js | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/handlers/adminProductHandler.js b/src/handlers/adminProductHandler.js index cd8f236..a6860b4 100644 --- a/src/handlers/adminProductHandler.js +++ b/src/handlers/adminProductHandler.js @@ -449,8 +449,11 @@ export default class AdminProductHandler { } 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'); + await fs.rm(fileContent); + } else if (msg.text) { jsonContent = msg.text; } else { @@ -503,9 +506,9 @@ export default class AdminProductHandler { this.userStates.delete(chatId); } catch (error) { - await db.runAsync('ROLLBACK'); console.error('Error importing products:', error); await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); + await db.runAsync('ROLLBACK'); } return true; diff --git a/src/index.js b/src/index.js index 1f832eb..58242f9 100644 --- a/src/index.js +++ b/src/index.js @@ -69,9 +69,7 @@ bot.onText(/\/admin/, async (msg) => { // Handle user menu buttons bot.on('message', async (msg) => { - if (!msg.text) return; - - if (msg.text.toLowerCase() === '/start') { + if (msg.text && msg.text.toLowerCase() === '/start') { return; } From df3149e59a927b87512caa8f2662c61d15d40384 Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Fri, 15 Nov 2024 07:31:46 +0300 Subject: [PATCH 3/5] product photo fix --- corrupt-photo.jpg | Bin 0 -> 8308 bytes src/handlers/adminProductHandler.js | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 corrupt-photo.jpg diff --git a/corrupt-photo.jpg b/corrupt-photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df39d0c13173163d0665d959684a47fee26dc1f6 GIT binary patch literal 8308 zcmdT}2V4``)}IuLfRsf9q=leZfFv{pq)LrT2LTn8kPIP`Mv4?0;NmJMVg-d=0bLhS zSW#3!T~Prm0%FGoc5tx(tD?YnXHo#)?!Mi(@BM!JO~}l-=brOF|8wr0Gf5b}J^TpL zoa;mNfiU2(#8^Vma0gW4oy>}ZAS!hRqzpk24pPTpp(r5PfcGSf0)zqAEbumv%3FYp z9G*OZK+G4x3<95(46_MwEH+GF(Ro}F!HdSBF<}COM;CKou86>5v&BM@fF@$`xCCJe zCzi)15yDtZE+JOHOMtlq22DgGEQ9GHo`7Tt0wOVG*WnI@_IQzq??fhZg(Mn-7Ymc< zJPtXT#wS~oC}halEtyZFC%_^?EX-taU5KwwoF)=j3>RXgeE=nZ?+M4V{89yQSZZK6 zJvD*u$RN78syHV*C3E;37z9q{u(?8~WEUa|?gV5+O(wdkjB^$+;+#Uge55vDoSOE(wVG377OSa+)%@7C)h8HZP3s`)#0thB8mM0du z5Q$P?emqaa6UOuSa`-X1%BRtcWlmJSh;9dx04o8@{Xh&lN=hhVkVjjfu4t-cB7q={ z>kErSDO6;&guYata5)M{9A0ojH^aj>G;SlKwrg8{Hm z7BgNnCgQ*D906(ryyWF|@)W=f76^*gg&-*mnH47D(GyT3BH@dQIe7?$u!!pALbSGZ zbhLMHaF}I-kr*ZhR5w(yf7SLfACXdHt!*VShOw+*4<${z9tP98wRNBc4 z++zVOE{n^Q*Yr2mVHuB3izRynP_2SPgZ!v-sNvpTR^Cyb-ofEv-X5Wze%^t;)Ie`) zpqFBG;s5}qW4P}oG204ELZ9>8kewumOx{}%Tnm^&SYfs1HD8* zEy<((fy@}WRU|MTY>^BP*&I%68j?e@6$6f=iP<8e>;I}1eZdyxfv_FTP{kRou0OhN zzfl1Kk}#hp5W?XpeAs1bC@fjEsjg!k=Y{&aB3u3UwgbF z9BKd_k3L~?46(tG#uq!t5St7|{jV0>5X*)1e(&BNZihS}6(uEQB}ElwWo1}&2n*_J$x2Y zRmL)~3=BpK8m@;7z_SjAJde?g(GyNlNdb?=4Bv&6Q8lI}ZDjHvCn=HNK#*Iil;RaS z1ZkAJ^+{;j>eWHG?w~=*NUhPW;sxYroW%7FAhRX9EA#??NI_}GHJ2rh%kS)wDD^i8 zS!qzwySu&zo|Bi`#h_Z8bq!y7WcFkp$ zagjscysSs45&=lRPZ6Sc?EZg7NaK(nX}b<)`rWRJO3A&$J44!!PuJMh3q0^ zLaouUs;(n+$MUG)6pq<-s3Lq~`!r+%&Br19hwpDrFkZlGQL0biY(w-YAHx};%?Thv^hpRHR^C1&Ai&3^<)?)i;JD||Hl+g|&ev(8WNad*q=WrULK$wDy zf}rTw9d|?_DGT%kWk4jl)aqC;Vj-z9^aXb?5jjJ36cJ2sG^V91?F;2Z2KF-N3s5Ifw1~2=GuMsH7jx<3U^nzG z`N6~Be?(<6K&c$;Vfj)bHI)}X1`MQI0-FtPB?v?70q6e*hZ|^L(=@?W!YbmBCMKnf z!|T{$6)>9G6YPQs6nE>dY>G^!y$qv(9fl06?}c_`Z*hN;>o{4Riu;qo*5fuoc^VIH zUv!OjTTY)mq3v@S)7kcW{bsf2YlY_=I||=iO&+ay)VY*!uk@(VA~3(^jRsyR1Cul- z!1joZyZ=yk-t5WuXp#LFP5x-S=`jhD2enseM16a3&cYotf?lcRYP;1wy?Sw$!5OB8 z#gf^F5P zZ;_T!s~;ojv}h=z*2Y9}TVFR)cO?)jdhP(Ww#9{TS7Dkag2;M6uDH z_T4VmQc#n@ynMkC-y8ZMx5K^a5%B|iK-#}9rd^T*8oGSNasHmJ4WPVtQogmYTZyFg zi7ZQH7=q>eU!6R+U4l8V^!$fwz3wSR&Mn6A8g@#%a7~zJX`7d+boD)u0S}t{zBodC zIpX8LsWonEn>0ZoHOQ8#wE)sXAY=;&jl$IvF747tM?x zDEp3l)ismi#npV;$j4`73y z(AmHLGFaMCe_CtqMZZTSoz}-323jbVF;R2JS#<8{*fryjcf&1#RaCIU(Zsl{gIbB= zd)W~a)UBfDmmI)HFG?cb$kZQ(t_Kb6`Dpz5K>NpP{=j;Z+kQiJ1;3thUiDe16QNvP z+v9yeb$fg4F5}p;GrGEqE*zx^PO!r~ugr@gc6QfHJV=^H7|J5yx=#7tNZs_r_WaEy z?@YYnUDvftx9Iz<`b(aS#3F_8lBYESS>Tp=b4d%)$)+yO;;sGT8mH2c^M>9Y%OW1rf@^UQgd>Mb?W-;>;8*NV*9mqOY={8HFTlMO*?(W4r9z; zMOQ>1iCXhXZj`!mMTK~yPM;-#Ooz#RdOAg@f@0y3zCcZ-B_?7GwrY%0W4*rjrKd|l zrIX6<>so9t0}JrrxJU|_dcRdrvj8bTchX-o6KxoVu+*-}-ugTYI9prGHd@rFG1Q%MJIB`{p4L%7@0I%##*GCc*g$4(XgzZ=kj8w?=1STdvJqN>EjnJ zsgnbBjd7SRigD4kJAAVdqy7^mPX$N8t~znECwn`(l&=@cR`8S@>krJ<3`4Esnm+#5 z|M01XbARSlHh|Tu@B$noiGrH>yN|#BoSRoOZux)rq_A~=M?JXjnn8=14q6OO35N&Y ztYjagFmE)qLW(O}-%^5U1XF8wkHqvdMY7K&Zl>UK$-Z+*y0GS)mOuM48-H;Xx7TFB z@}&HMw)T?^-|D6~YN-|9hSkqaTc+=M=#tUe;8!-^(zeb1QmMY;LD(DC3WjnoGL^z@ zUOknTX|Z8?^u*{#DLU_|**a!FdCyturEhv-S8Rh{cWdyj&5C~9XZe+(PJQ9#dp*8M z$ui5`P0!sObHUfO;MJfZegOO_ZOc*ne7dPo=Ayo{Ak4J-O4NR%)EU~Lrfs%M8ME|d z*iMVj-!r6it2xzONNh@Ue)sw|BT`@IjCtk7>5SX2*%F)`EIjTngD5JV zSoE9=Xt;Pi&hdITs28dis_QXjn2lmee(G6LfySB8nPaPDazIKH(mjqM*?)}tw&eoyY($WB8 zy%$wC!)(L#xIOmzrA#xlfF%@2x#@)FDXg~So$|ol%BmEhb8X!spsInyU#4cH`Q)kd->&2 zP(2gz)bV+32_S3Nu0R9wzFXF@dskkUvHi4PmdyK5z7+0Z&vVT1aHncDV;IWK&xjqj&YU2<%pdRaApN0-);B)t zJ^h_B9wHs$Mc2LBZ*R)T8Y%c44%(0ZES6{F@y5OPwSK&*R)@joKL3cocZt>T9xFvU zr8nhtxs2JL&Fqf~C7{gGa&~LYP}7WAgV!URQB`rUVU~(*7#u`Z@dJw`W1D>2m;huPeC{MnBE-E{@n1=xV<&ULr!)-v597kzbi>B`NY_G))- zu3)KMT~$##rRw@8v-f^e8_c4w=@$+z5`E38FNpH)3h`R23w>v`ynKkv3*xz}S*k0f zyG6uW%iK=u?>M;Tt)2|_;fBi*9v?O&?DDkTeV}4Jul{Lz=4^wB75*8|im?|L@uAP; z&`brjyu+vJ2B)fT6#B&p`^BwRPke1^HR6mWJ5qJsOgemKhHT7@T8+UpH1ye+SOJPD7$5%Ri%>4tb=yPntow^j{jh!}nRmQv1AC8w zY5VOuRjjHS2?1%oy%?l;@)WbSvf}JxG{a0J&gGlS4@`O`H>nn%t%EfBeU68eUr!KE zx<5u}2}$8AgMBLAxJ(x%`idOc^5PaP5tO2N`_;C}^<`!@ni0&Cq;{W z;x~)@{BpVrW?77cnK9*Fex!4`oK0bAo`4qp?x-AFfd4Sv*$eAg>KgA{@+r+w*HE~) z;7lq{;pIu)eQ!pIUdk+u>kgUKMjpMvD{dU6sJ`+}9~@ehIzmnB>R#0x|E(%#FZPaP zbRkDfh!!yNBPIrYKT-&fwru+oTez#kgG3gO(uN*?da`qi=8m(ghPDNXweQczS06W6 z`2P8SR-%7bd$p1b1IV(oczNmKF5TM9p8rh9YV*ocdU5+EvWA5!vU8M?=9>4|)!XyJ hNc$G~*X|VCbivmz@BeGV(6jTMW1vd6cg*2C{|BE4>DT}O literal 0 HcmV?d00001 diff --git a/src/handlers/adminProductHandler.js b/src/handlers/adminProductHandler.js index a6860b4..6e8bc2d 100644 --- a/src/handlers/adminProductHandler.js +++ b/src/handlers/adminProductHandler.js @@ -564,17 +564,22 @@ Coordinates: ${product.hidden_coordinates} // Send product photos if (product.photo_url) { - await this.bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' }); + try { + 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) { - await this.bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' }); + try { + 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, { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - }); + await this.bot.deleteMessage(chatId, messageId); + await this.bot.sendMessage(chatId, message, { reply_markup: keyboard }); } catch (error) { console.error('Error in handleViewProduct:', error); await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.'); From e1eda05afe34a28c9bbdf93a3581ed23ce3be445 Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Fri, 15 Nov 2024 08:19:54 +0300 Subject: [PATCH 4/5] products pagination --- src/handlers/adminProductHandler.js | 1107 +++++++++++++++------------ src/index.js | 3 + 2 files changed, 602 insertions(+), 508 deletions(-) diff --git a/src/handlers/adminProductHandler.js b/src/handlers/adminProductHandler.js index 6e8bc2d..a5ef0ab 100644 --- a/src/handlers/adminProductHandler.js +++ b/src/handlers/adminProductHandler.js @@ -3,539 +3,627 @@ import config from '../config/config.js'; import fs from 'fs/promises'; export default class AdminProductHandler { - constructor(bot) { - this.bot = bot; - this.userStates = new Map(); - } - - isAdmin(userId) { - return config.ADMIN_IDS.includes(userId.toString()); - } - - async handleProductManagement(msg) { - const chatId = msg.chat?.id || msg.message?.chat.id; - - if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) { - await this.bot.sendMessage(chatId, 'Unauthorized access.'); - return; + constructor(bot) { + this.bot = bot; + this.userStates = new Map(); } - try { - const countries = await db.allAsync( - 'SELECT DISTINCT country FROM locations ORDER BY country' - ); + isAdmin(userId) { + return config.ADMIN_IDS.includes(userId.toString()); + } - if (countries.length === 0) { - await this.bot.sendMessage( - chatId, - 'No locations available. Please add locations first.', - { - reply_markup: { - inline_keyboard: [[ - { text: '📍 Manage Locations', callback_data: 'view_locations' } - ]] + async handleProductManagement(msg) { + const chatId = msg.chat?.id || msg.message?.chat.id; + + if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) { + await this.bot.sendMessage(chatId, 'Unauthorized access.'); + return; + } + + try { + const countries = await db.allAsync( + 'SELECT DISTINCT country FROM locations ORDER BY country' + ); + + if (countries.length === 0) { + await this.bot.sendMessage( + chatId, + 'No locations available. Please add locations first.', + { + reply_markup: { + inline_keyboard: [[ + {text: '📍 Manage Locations', callback_data: 'view_locations'} + ]] + } + } + ); + return; + } + + const keyboard = { + inline_keyboard: countries.map(loc => [{ + text: loc.country, + callback_data: `prod_country_${loc.country}` + }]) + }; + + await this.bot.sendMessage( + chatId, + '🌍 Select country to manage products:', + {reply_markup: keyboard} + ); + } catch (error) { + console.error('Error in handleProductManagement:', error); + await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.'); + } + } + + async handleCountrySelection(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const country = callbackQuery.data.replace('prod_country_', ''); + + try { + const cities = await db.allAsync( + 'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city', + [country] + ); + + const keyboard = { + inline_keyboard: [ + ...cities.map(loc => [{ + text: loc.city, + callback_data: `prod_city_${country}_${loc.city}` + }]), + [{text: '« Back', callback_data: 'manage_products'}] + ] + }; + + await this.bot.editMessageText( + `🏙 Select city in ${country}:`, + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleCountrySelection:', error); + await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.'); + } + } + + async handleCitySelection(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [country, city] = callbackQuery.data.replace('prod_city_', '').split('_'); + + try { + const districts = await db.allAsync( + 'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district', + [country, city] + ); + + const keyboard = { + inline_keyboard: [ + ...districts.map(loc => [{ + text: loc.district, + callback_data: `prod_district_${country}_${city}_${loc.district}` + }]), + [{text: '« Back', callback_data: `prod_country_${country}`}] + ] + }; + + await this.bot.editMessageText( + `📍 Select district in ${city}:`, + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleCitySelection:', error); + await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.'); + } + } + + async handleDistrictSelection(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_'); + + try { + const location = await db.getAsync( + 'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?', + [country, city, district] + ); + + if (!location) { + throw new Error('Location not found'); + } + + const categories = await db.allAsync( + 'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name', + [location.id] + ); + + const keyboard = { + inline_keyboard: [ + ...categories.map(cat => [{ + text: cat.name, + callback_data: `prod_category_${location.id}_${cat.id}` + }]), + [{text: '➕ Add Category', callback_data: `add_category_${location.id}`}], + [{text: '« Back', callback_data: `prod_city_${country}_${city}`}] + ] + }; + + await this.bot.editMessageText( + '📦 Select or add category:', + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleDistrictSelection:', error); + await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.'); + } + } + + async handleCategoryInput(msg) { + const chatId = msg.chat.id; + const state = this.userStates.get(chatId); + + if (!state || !state.action?.startsWith('add_category_')) return false; + + try { + const locationId = state.action.replace('add_category_', ''); + + await db.runAsync( + 'INSERT INTO categories (location_id, name) VALUES (?, ?)', + [locationId, msg.text] + ); + + const location = await db.getAsync( + 'SELECT country, city, district FROM locations WHERE id = ?', + [locationId] + ); + + await this.bot.sendMessage( + chatId, + `✅ Category "${msg.text}" added successfully!`, + { + reply_markup: { + inline_keyboard: [[ + { + text: '« Back to Categories', + callback_data: `prod_district_${location.country}_${location.city}_${location.district}` + } + ]] + } + } + ); + + this.userStates.delete(chatId); + } catch (error) { + if (error.code === 'SQLITE_CONSTRAINT') { + await this.bot.sendMessage(chatId, 'This category already exists in this location.'); + } else { + console.error('Error adding category:', error); + await this.bot.sendMessage(chatId, 'Error adding category. Please try again.'); + } + } + + return true; + } + + async handleAddCategory(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const locationId = callbackQuery.data.replace('add_category_', ''); + + this.userStates.set(chatId, {action: `add_category_${locationId}`}); + + await this.bot.editMessageText( + 'Please enter the name for the new category:', + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: { + inline_keyboard: [[ + {text: '❌ Cancel', callback_data: `prod_district_${locationId}`} + ]] + } } - } ); - return; - } - - const keyboard = { - inline_keyboard: countries.map(loc => [{ - text: loc.country, - callback_data: `prod_country_${loc.country}` - }]) - }; - - await this.bot.sendMessage( - chatId, - '🌍 Select country to manage products:', - { reply_markup: keyboard } - ); - } catch (error) { - console.error('Error in handleProductManagement:', error); - await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.'); - } - } - - async handleCountrySelection(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const country = callbackQuery.data.replace('prod_country_', ''); - - try { - const cities = await db.allAsync( - 'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city', - [country] - ); - - const keyboard = { - inline_keyboard: [ - ...cities.map(loc => [{ - text: loc.city, - callback_data: `prod_city_${country}_${loc.city}` - }]), - [{ text: '« Back', callback_data: 'manage_products' }] - ] - }; - - await this.bot.editMessageText( - `🏙 Select city in ${country}:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCountrySelection:', error); - await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.'); - } - } - - async handleCitySelection(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [country, city] = callbackQuery.data.replace('prod_city_', '').split('_'); - - try { - const districts = await db.allAsync( - 'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district', - [country, city] - ); - - const keyboard = { - inline_keyboard: [ - ...districts.map(loc => [{ - text: loc.district, - callback_data: `prod_district_${country}_${city}_${loc.district}` - }]), - [{ text: '« Back', callback_data: `prod_country_${country}` }] - ] - }; - - await this.bot.editMessageText( - `📍 Select district in ${city}:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCitySelection:', error); - await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.'); - } - } - - async handleDistrictSelection(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_'); - - try { - const location = await db.getAsync( - 'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?', - [country, city, district] - ); - - if (!location) { - throw new Error('Location not found'); - } - - const categories = await db.allAsync( - 'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name', - [location.id] - ); - - const keyboard = { - inline_keyboard: [ - ...categories.map(cat => [{ - text: cat.name, - callback_data: `prod_category_${location.id}_${cat.id}` - }]), - [{ text: '➕ Add Category', callback_data: `add_category_${location.id}` }], - [{ text: '« Back', callback_data: `prod_city_${country}_${city}` }] - ] - }; - - await this.bot.editMessageText( - '📦 Select or add category:', - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleDistrictSelection:', error); - await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.'); - } - } - - async handleCategoryInput(msg) { - const chatId = msg.chat.id; - const state = this.userStates.get(chatId); - - if (!state || !state.action?.startsWith('add_category_')) return false; - - try { - const locationId = state.action.replace('add_category_', ''); - - await db.runAsync( - 'INSERT INTO categories (location_id, name) VALUES (?, ?)', - [locationId, msg.text] - ); - - const location = await db.getAsync( - 'SELECT country, city, district FROM locations WHERE id = ?', - [locationId] - ); - - await this.bot.sendMessage( - chatId, - `✅ Category "${msg.text}" added successfully!`, - { - reply_markup: { - inline_keyboard: [[ - { text: '« Back to Categories', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` } - ]] - } - } - ); - - this.userStates.delete(chatId); - } catch (error) { - if (error.code === 'SQLITE_CONSTRAINT') { - await this.bot.sendMessage(chatId, 'This category already exists in this location.'); - } else { - console.error('Error adding category:', error); - await this.bot.sendMessage(chatId, 'Error adding category. Please try again.'); - } } - return true; - } + async handleCategorySelection(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_'); - async handleAddCategory(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const locationId = callbackQuery.data.replace('add_category_', ''); - - this.userStates.set(chatId, { action: `add_category_${locationId}` }); - - await this.bot.editMessageText( - 'Please enter the name for the new category:', - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: { - inline_keyboard: [[ - { text: '❌ Cancel', callback_data: `prod_district_${locationId}` } - ]] + try { + const subcategories = await db.allAsync( + 'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name', + [categoryId] + ); + + const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]); + const location = await db.getAsync('SELECT country, city, district FROM locations WHERE id = ?', [locationId]); + + const keyboard = { + inline_keyboard: [ + ...subcategories.map(sub => [{ + text: sub.name, + callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}` + }]), + [{text: '➕ Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}`}], + [{text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}`}], + [{ + text: '« Back', + callback_data: `prod_district_${location.country}_${location.city}_${location.district}` + }] + ] + }; + + await this.bot.editMessageText( + `📦 Category: ${category.name}\nSelect or add subcategory:`, + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleCategorySelection:', error); + await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.'); } - } - ); - } - - async handleCategorySelection(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_'); - - try { - const subcategories = await db.allAsync( - 'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name', - [categoryId] - ); - - const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]); - const location = await db.getAsync('SELECT country, city, district FROM locations WHERE id = ?', [locationId]); - - const keyboard = { - inline_keyboard: [ - ...subcategories.map(sub => [{ - text: sub.name, - callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}` - }]), - [{ text: '➕ Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}` }], - [{ text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}` }], - [{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] - ] - }; - - await this.bot.editMessageText( - `📦 Category: ${category.name}\nSelect or add subcategory:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCategorySelection:', error); - await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.'); - } - } - - async handleSubcategoryInput(msg) { - const chatId = msg.chat.id; - const state = this.userStates.get(chatId); - - if (!state || !state.action?.startsWith('add_subcategory_')) return false; - - try { - const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_'); - - await db.runAsync( - 'INSERT INTO subcategories (category_id, name) VALUES (?, ?)', - [categoryId, msg.text] - ); - - await this.bot.sendMessage( - chatId, - `✅ Subcategory "${msg.text}" added successfully!`, - { - reply_markup: { - inline_keyboard: [[ - { text: '« Back to Subcategories', callback_data: `prod_category_${locationId}_${categoryId}` } - ]] - } - } - ); - - this.userStates.delete(chatId); - } catch (error) { - if (error.code === 'SQLITE_CONSTRAINT') { - await this.bot.sendMessage(chatId, 'This subcategory already exists in this category.'); - } else { - console.error('Error adding subcategory:', error); - await this.bot.sendMessage(chatId, 'Error adding subcategory. Please try again.'); - } } - return true; - } + async handleSubcategoryInput(msg) { + const chatId = msg.chat.id; + const state = this.userStates.get(chatId); - async handleAddSubcategory(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const [locationId, categoryId] = callbackQuery.data.replace('add_subcategory_', '').split('_'); - - this.userStates.set(chatId, { action: `add_subcategory_${locationId}_${categoryId}` }); - - await this.bot.editMessageText( - 'Please enter the name for the new subcategory:', - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: { - inline_keyboard: [[ - { text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}` } - ]] + if (!state || !state.action?.startsWith('add_subcategory_')) return false; + + try { + const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_'); + + await db.runAsync( + 'INSERT INTO subcategories (category_id, name) VALUES (?, ?)', + [categoryId, msg.text] + ); + + await this.bot.sendMessage( + chatId, + `✅ Subcategory "${msg.text}" added successfully!`, + { + reply_markup: { + inline_keyboard: [[ + { + text: '« Back to Subcategories', + callback_data: `prod_category_${locationId}_${categoryId}` + } + ]] + } + } + ); + + this.userStates.delete(chatId); + } catch (error) { + if (error.code === 'SQLITE_CONSTRAINT') { + await this.bot.sendMessage(chatId, 'This subcategory already exists in this category.'); + } else { + console.error('Error adding subcategory:', error); + await this.bot.sendMessage(chatId, 'Error adding subcategory. 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('_'); + return true; + } - try { - const products = await db.allAsync( - `SELECT id, name, price, quantity_in_stock + async handleAddSubcategory(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const [locationId, categoryId] = callbackQuery.data.replace('add_subcategory_', '').split('_'); + + this.userStates.set(chatId, {action: `add_subcategory_${locationId}_${categoryId}`}); + + await this.bot.editMessageText( + 'Please enter the name for the new subcategory:', + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: { + inline_keyboard: [[ + {text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}`} + ]] + } + } + ); + } + + async viewProductsPage(locationId, categoryId, subcategoryId, page) { + 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( + `SELECT id, name, price, quantity_in_stock FROM products WHERE location_id = ? AND category_id = ? AND subcategory_id = ? - ORDER BY name`, - [locationId, categoryId, subcategoryId] - ); + ORDER BY name + LIMIT ? + OFFSET ? + `, + [locationId, categoryId, subcategoryId, limit, offset] + ); - const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]); - const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]); + if ((products.length === 0) && (page == 0)) { + return { + text: 'No products for this location', + markup: { + inline_keyboard: [ + [{text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}`}] + ] + } + }; + } - const keyboard = { - inline_keyboard: [ - ...products.map(prod => [{ - text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`, - callback_data: `view_product_${prod.id}` - }]), - [{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}` }], - [{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }] - ] - }; + if ((products.length === 0) && (page > 0)) { + return await this.viewProductsPage(locationId, categoryId, subcategoryId, previousPage); + } - await this.bot.editMessageText( - `📦 ${category.name} > ${subcategory.name}\nSelect product or import new ones:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard + 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 keyboard = { + inline_keyboard: [ + ...products.map(prod => [{ + text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`, + callback_data: `view_product_${prod.id}` + }]), + [{ + 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.'}; } - ); - } catch (error) { - console.error('Error in handleSubcategorySelection:', error); - await this.bot.sendMessage(chatId, 'Error loading products. Please try again.'); } - } - async handleAddProduct(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_'); + 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 location = await db.getAsync( - 'SELECT country, city, district FROM locations WHERE id = ?', - [locationId] - ); - const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]); - const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]); + try { + const {text, markup} = await this.viewProductsPage(locationId, categoryId, subcategoryId, 0); - const sampleProducts = [ - { - name: "Sample Product 1", - price: 100, - description: "Product description", - private_data: "Hidden details about the product", - quantity_in_stock: 10, - photo_url: "https://example.com/photo.jpg", - hidden_photo_url: "https://example.com/hidden.jpg", - hidden_coordinates: "40.7128,-74.0060", - hidden_description: "Secret location details" + await this.bot.editMessageText( + text, + { + chat_id: chatId, + message_id: messageId, + reply_markup: markup + } + ); + } catch (error) { + console.error('Error in handleSubcategorySelection:', error); + await this.bot.sendMessage(chatId, 'Error loading products. Please try again.'); } - ]; - - const jsonExample = JSON.stringify(sampleProducts, null, 2); - const message = `To import products, send a JSON file with an array of products in the following format:\n\n
${jsonExample}
\n\nEach product must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; - - this.userStates.set(chatId, { - action: 'import_products', - locationId, - categoryId, - subcategoryId - }); - - await this.bot.editMessageText(message, { - chat_id: chatId, - message_id: messageId, - parse_mode: 'HTML', - reply_markup: { - inline_keyboard: [[ - { text: '❌ Cancel', callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` } - ]] - } - }); - } catch (error) { - console.error('Error in handleAddProduct:', error); - await this.bot.sendMessage(chatId, 'Error preparing product import. Please try again.'); } - } - async handleProductImport(msg) { - const chatId = msg.chat.id; - const state = this.userStates.get(chatId); - - if (!state || state.action !== 'import_products') return false; - - try { - let products; - let jsonContent; - - // Handle file upload - if (msg.document) { - if (!msg.document.file_name.endsWith('.json')) { - await this.bot.sendMessage(chatId, 'Please upload a .json file.'); - return true; + async handleProductListPage(callbackQuery) { + if (!this.isAdmin(callbackQuery.from.id)) { + return; } - const file = await this.bot.getFile(msg.document.file_id); + const chatId = callbackQuery.message.chat.id; - const fileContent = await this.bot.downloadFile(file.file_id, '.'); - jsonContent = await fs.readFile(fileContent, 'utf8'); - await fs.rm(fileContent); + const [locationId, categoryId, subcategoryId, page] = callbackQuery.data.replace('list_products_', '').split("_"); - } else if (msg.text) { - jsonContent = msg.text; - } else { - await this.bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); - return true; - } - - try { - products = JSON.parse(jsonContent); - if (!Array.isArray(products)) { - throw new Error('Input must be an array of products'); + 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; } - } catch (e) { - await this.bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.'); - return true; - } + } - await db.runAsync('BEGIN TRANSACTION'); + async handleAddProduct(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_'); - for (const product of products) { - await db.runAsync( - `INSERT INTO products ( + try { + const location = await db.getAsync( + 'SELECT country, city, district FROM locations WHERE id = ?', + [locationId] + ); + const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]); + const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]); + + const sampleProducts = [ + { + name: "Sample Product 1", + price: 100, + description: "Product description", + private_data: "Hidden details about the product", + quantity_in_stock: 10, + photo_url: "https://example.com/photo.jpg", + hidden_photo_url: "https://example.com/hidden.jpg", + hidden_coordinates: "40.7128,-74.0060", + hidden_description: "Secret location details" + } + ]; + + const jsonExample = JSON.stringify(sampleProducts, null, 2); + const message = `To import products, send a JSON file with an array of products in the following format:\n\n
${jsonExample}
\n\nEach product must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; + + this.userStates.set(chatId, { + action: 'import_products', + locationId, + categoryId, + subcategoryId + }); + + await this.bot.editMessageText(message, { + chat_id: chatId, + message_id: messageId, + parse_mode: 'HTML', + reply_markup: { + inline_keyboard: [[ + { + text: '❌ Cancel', + callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` + } + ]] + } + }); + } catch (error) { + console.error('Error in handleAddProduct:', error); + await this.bot.sendMessage(chatId, 'Error preparing product import. Please try again.'); + } + } + + async handleProductImport(msg) { + const chatId = msg.chat.id; + const state = this.userStates.get(chatId); + + if (!state || state.action !== 'import_products') return false; + + try { + let products; + let jsonContent; + + // Handle file upload + if (msg.document) { + if (!msg.document.file_name.endsWith('.json')) { + await this.bot.sendMessage(chatId, 'Please upload a .json file.'); + return true; + } + + const file = await this.bot.getFile(msg.document.file_id); + + const fileContent = await this.bot.downloadFile(file.file_id, '.'); + jsonContent = await fs.readFile(fileContent, 'utf8'); + await fs.rm(fileContent); + + } else if (msg.text) { + jsonContent = msg.text; + } else { + await this.bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); + return true; + } + + try { + products = JSON.parse(jsonContent); + if (!Array.isArray(products)) { + throw new Error('Input must be an array of products'); + } + } catch (e) { + await this.bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.'); + return true; + } + + await db.runAsync('BEGIN TRANSACTION'); + + for (const product of products) { + await db.runAsync( + `INSERT INTO products ( location_id, category_id, subcategory_id, name, price, description, private_data, quantity_in_stock, photo_url, hidden_photo_url, hidden_coordinates, hidden_description ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - state.locationId, state.categoryId, state.subcategoryId, - product.name, product.price, product.description, product.private_data, - product.quantity_in_stock, product.photo_url, product.hidden_photo_url, - product.hidden_coordinates, product.hidden_description - ] - ); - } + [ + state.locationId, state.categoryId, state.subcategoryId, + product.name, product.price, product.description, product.private_data, + product.quantity_in_stock, product.photo_url, product.hidden_photo_url, + product.hidden_coordinates, product.hidden_description + ] + ); + } - await db.runAsync('COMMIT'); + await db.runAsync('COMMIT'); - await this.bot.sendMessage( - chatId, - `✅ Successfully imported ${products.length} products!`, - { - reply_markup: { - inline_keyboard: [[ - { text: '« Back to Products', callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` } - ]] - } + await this.bot.sendMessage( + chatId, + `✅ Successfully imported ${products.length} products!`, + { + reply_markup: { + inline_keyboard: [[ + { + text: '« Back to Products', + callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` + } + ]] + } + } + ); + + this.userStates.delete(chatId); + } catch (error) { + console.error('Error importing products:', error); + await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); + await db.runAsync('ROLLBACK'); } - ); - this.userStates.delete(chatId); - } catch (error) { - console.error('Error importing products:', error); - await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); - await db.runAsync('ROLLBACK'); + return true; } - return true; - } + async handleViewProduct(callbackQuery) { + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const productId = callbackQuery.data.replace('view_product_', ''); - async handleViewProduct(callbackQuery) { - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const productId = callbackQuery.data.replace('view_product_', ''); - - try { - const product = await db.getAsync( - `SELECT p.*, c.name as category_name, s.name as subcategory_name, + 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] - ); + [productId] + ); - if (!product) { - throw new Error('Product not found'); - } + if (!product) { + throw new Error('Product not found'); + } - const message = ` + const message = ` 📦 Product Details: Name: ${product.name} @@ -552,37 +640,40 @@ Hidden Location: ${product.hidden_description} Coordinates: ${product.hidden_coordinates} `; - const keyboard = { - inline_keyboard: [ - [ - { text: '✏️ Edit', callback_data: `edit_product_${productId}` }, - { text: '❌ Delete', callback_data: `delete_product_${productId}` } - ], - [{ text: '« Back', callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}` }] - ] - }; + const keyboard = { + inline_keyboard: [ + [ + {text: '✏️ Edit', callback_data: `edit_product_${productId}`}, + {text: '❌ Delete', callback_data: `delete_product_${productId}`} + ], + [{ + text: '« Back', + callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}` + }] + ] + }; - // Send product photos - if (product.photo_url) { - try { - 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) { - try { - 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' }) - } - } + // Send product photos + if (product.photo_url) { + try { + 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) { + try { + 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.deleteMessage(chatId, messageId); - await this.bot.sendMessage(chatId, message, { reply_markup: keyboard }); - } catch (error) { - console.error('Error in handleViewProduct:', error); - await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.'); + await this.bot.deleteMessage(chatId, messageId); + await this.bot.sendMessage(chatId, message, {reply_markup: keyboard}); + } catch (error) { + console.error('Error in handleViewProduct:', error); + await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.'); + } } - } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index 58242f9..f79f192 100644 --- a/src/index.js +++ b/src/index.js @@ -269,6 +269,9 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action.startsWith('prod_subcategory_')) { logDebug(action, 'handleSubcategorySelection'); await adminProductHandler.handleSubcategorySelection(callbackQuery); + } else if (action.startsWith('list_products_')) { + logDebug(action, 'handleSubcategorySelection'); + await adminProductHandler.handleProductListPage(callbackQuery); } else if (action.startsWith('add_product_')) { logDebug(action, 'handleAddProduct'); await adminProductHandler.handleAddProduct(callbackQuery); From 89a7a8b9c5072e58f155ab1497cc80f71c758d45 Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Fri, 15 Nov 2024 09:15:44 +0300 Subject: [PATCH 5/5] product deletion --- src/handlers/adminProductHandler.js | 104 ++++++++++++++++++++++++++++ src/index.js | 6 ++ 2 files changed, 110 insertions(+) diff --git a/src/handlers/adminProductHandler.js b/src/handlers/adminProductHandler.js index a5ef0ab..65587f1 100644 --- a/src/handlers/adminProductHandler.js +++ b/src/handlers/adminProductHandler.js @@ -1,6 +1,7 @@ import db from '../config/database.js'; import config from '../config/config.js'; import fs from 'fs/promises'; +import User from "../models/User.js"; export default class AdminProductHandler { constructor(bot) { @@ -676,4 +677,107 @@ Coordinates: ${product.hidden_coordinates} 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.'); + } + } + } \ No newline at end of file diff --git a/src/index.js b/src/index.js index f79f192..72c16ef 100644 --- a/src/index.js +++ b/src/index.js @@ -278,6 +278,12 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action.startsWith('view_product_')) { logDebug(action, 'handleViewProduct'); 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 else if (action.startsWith('view_user_')) {