diff --git a/src/handlers/adminHandlers/adminProductHandler.js b/src/handlers/adminHandlers/adminProductHandler.js deleted file mode 100644 index 9ad79f9..0000000 --- a/src/handlers/adminHandlers/adminProductHandler.js +++ /dev/null @@ -1,1093 +0,0 @@ -import db from '../../config/database.js'; -import { isAdmin } from '../../middleware/auth.js'; -import fs from 'fs/promises'; -import LocationService from "../../services/locationService.js"; -import bot from "../../context/bot.js"; -import CategoryService from "../../services/categoryService.js"; -import userStates from "../../context/userStates.js"; -import ProductService from "../../services/productService.js"; -import Validators from '../../utils/validators.js'; - -export default class AdminProductHandler { - - static async handleProductManagement(msg) { - const chatId = msg.chat?.id || msg.message?.chat.id; - - if (!isAdmin(msg.from?.id || msg.message?.from.id)) { - await bot.sendMessage(chatId, 'Unauthorized access.'); - return; - } - - try { - const countries = await LocationService.getCountries() - - if (countries.length === 0) { - await 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 bot.sendMessage( - chatId, - '🌍 Select country to manage products:', - {reply_markup: keyboard} - ); - } catch (error) { - console.error('Error in handleProductManagement:', error); - await bot.sendMessage(chatId, 'Error loading locations. Please try again.'); - } - } - - static async handleCountrySelection(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const country = callbackQuery.data.replace('prod_country_', ''); - - try { - const cities = await LocationService.getCitiesByCountry(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 bot.editMessageText( - `πŸ™ Select city in ${country}:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCountrySelection:', error); - await bot.sendMessage(chatId, 'Error loading cities. Please try again.'); - } - } - - static async handleCitySelection(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - 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 LocationService.getDistrictsByCountryAndCity(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 bot.editMessageText( - `πŸ“ Select district in ${city}:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCitySelection:', error); - await bot.sendMessage(chatId, 'Error loading districts. Please try again.'); - } - } - - static async handleDistrictSelection(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_'); - - userStates.delete(chatId); - - try { - const location = await LocationService.getLocation(country, city, district); - - if (!location) { - throw new Error('Location not found'); - } - - const categories = await CategoryService.getCategoriesByLocationId(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 bot.editMessageText( - 'πŸ“¦ Select or add category:', - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleDistrictSelection:', error); - await bot.sendMessage(chatId, 'Error loading categories. Please try again.'); - } - } - - static async handleCategoryInput(msg) { - const chatId = msg.chat.id; - const state = userStates.get(chatId); - - if (!state || !state.action?.startsWith('add_category_')) { - return false; - } - - if (!isAdmin(msg.from.id)) { - await bot.sendMessage(chatId, 'Unauthorized access.'); - return; - } - - try { - const locationId = state.action.replace('add_category_', ''); - - if (!Validators.isValidString(msg.text, 255)) { - await bot.sendMessage(chatId, 'Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ'); - return true; - } - - await db.runAsync( - 'INSERT INTO categories (location_id, name) VALUES (?, ?)', - [locationId, msg.text] - ); - - const location = await LocationService.getLocationById(locationId); - - await 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}` - } - ]] - } - } - ); - - userStates.delete(chatId); - } catch (error) { - if (error.code === 'SQLITE_CONSTRAINT') { - await bot.sendMessage(chatId, 'This category already exists in this location.'); - } else { - console.error('Error adding category:', error); - await bot.sendMessage(chatId, 'Error adding category. Please try again.'); - } - } - - return true; - } - - static async handleAddCategory(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const locationId = callbackQuery.data.replace('add_category_', ''); - - userStates.set(chatId, {action: `add_category_${locationId}`}); - - const location = await LocationService.getLocationById(locationId); - - await 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_${location.country}_${location.city}_${location.district}`} - ]] - } - } - ); - } - // ОбновлСниС ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ - static async handleCategoryUpdate(msg) { - const chatId = msg.chat.id; - const state = userStates.get(chatId); - - if (!state || !state.action?.startsWith('edit_category_')) { - console.log('[DEBUG] Invalid state or action:', state); - return false; - } - - if (!isAdmin(msg.from.id)) { - await bot.sendMessage(chatId, 'НСавторизованный доступ.'); - return; - } - - try { - const [locationId, categoryId] = state.action.replace('edit_category_', '').split('_'); - - console.log('[DEBUG] Updating category:', { locationId, categoryId, newName: msg.text }); - - if (!Validators.isValidString(msg.text, 255)) { - await bot.sendMessage(chatId, 'Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ'); - return true; - } - - await db.runAsync( - 'UPDATE categories SET name = ? WHERE id = ? AND location_id = ?', - [msg.text, categoryId, locationId] - ); - - console.log('[DEBUG] Category updated successfully'); - - await bot.sendMessage( - chatId, - `βœ… НазваниС ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΎ Π½Π° "${msg.text}".`, - { - reply_markup: { - inline_keyboard: [[ - { - text: 'Β« Назад ΠΊ катСгориям', - callback_data: `prod_category_${locationId}_${categoryId}` - } - ]] - } - } - ); - - userStates.delete(chatId); - } catch (error) { - console.error('[ERROR] Error updating category:', error); - await bot.sendMessage(chatId, 'Ошибка обновлСния ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ. ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΏΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ снова.'); - } - - return true; - } - // Π Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ - static async handleEditCategory(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const [locationId, categoryId] = callbackQuery.data.replace('edit_category_', '').split('_'); - - userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` }); - - await bot.editMessageText( - 'ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²Π²Π΅Π΄ΠΈΡ‚Π΅ Π½ΠΎΠ²ΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ:', - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: { - inline_keyboard: [[ - { text: '❌ ΠžΡ‚ΠΌΠ΅Π½Π°', callback_data: `prod_category_${locationId}_${categoryId}` } - ]] - } - } - ); - } - static async handleCategorySelection(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_'); - - try { - const category = await CategoryService.getCategoryById(categoryId); - const location = await LocationService.getLocationById(locationId); - - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Ρ‚ΠΎΠ²Π°Ρ€Ρ‹ для Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠΉ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ - const products = await ProductService.getProductsByCategoryId(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: 'βž• Add Product', callback_data: `add_product_${locationId}_${categoryId}` }], - [{ text: 'Β« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] - ] - }; - - await bot.editMessageText( - `πŸ“¦ Category: ${category.name}\nSelect or add product:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCategorySelection:', error); - await bot.sendMessage(chatId, 'Error loading products. Please try again.'); - } - } - - static async handleSubcategoryInput(msg) { - const chatId = msg.chat.id; - const state = userStates.get(chatId); - - if (!state || !state.action?.startsWith('add_subcategory_')) { - return false; - } - - if (!isAdmin(msg.from.id)) { - await bot.sendMessage(chatId, 'Unauthorized access.'); - return; - } - - try { - const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_'); - - if (!Validators.isValidString(msg.text, 255)) { - await bot.sendMessage(chatId, 'Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΏΠΎΠ΄ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ'); - return true; - } - - await db.runAsync( - 'INSERT INTO subcategories (category_id, name) VALUES (?, ?)', - [categoryId, msg.text] - ); - - await bot.sendMessage( - chatId, - `βœ… Subcategory "${msg.text}" added successfully!`, - { - reply_markup: { - inline_keyboard: [[ - { - text: 'Β« Back to Subcategories', - callback_data: `prod_category_${locationId}_${categoryId}` - } - ]] - } - } - ); - - userStates.delete(chatId); - } catch (error) { - if (error.code === 'SQLITE_CONSTRAINT') { - await bot.sendMessage(chatId, 'This subcategory already exists in this category.'); - } else { - console.error('Error adding subcategory:', error); - await bot.sendMessage(chatId, 'Error adding subcategory. Please try again.'); - } - } - - return true; - } - - static async handleAddSubcategory(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const [locationId, categoryId] = callbackQuery.data.replace('add_subcategory_', '').split('_'); - - userStates.set(chatId, {action: `add_subcategory_${locationId}_${categoryId}`}); - - await 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}`} - ]] - } - } - ); - } - - static 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 - LIMIT ? - OFFSET ? - `, - [locationId, categoryId, subcategoryId, limit, offset] - ); - - if ((products.length === 0) && (page == 0)) { - return { - text: 'No products for this location', - markup: { - inline_keyboard: [ - [{ - 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); - } - - 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.'}; - } - } - - static async handleCategorySelection(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_'); - - try { - const category = await CategoryService.getCategoryById(categoryId); - const location = await LocationService.getLocationById(locationId); - - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Ρ‚ΠΎΠ²Π°Ρ€Ρ‹ для Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠΉ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ - const products = await ProductService.getProductsByCategoryId(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: 'βž• Add Product', callback_data: `add_product_${locationId}_${categoryId}` }], - [{ text: 'Β« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] // Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ Π½Π° ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΡŽ - ] - }; - - await bot.editMessageText( - `πŸ“¦ Category: ${category.name}\nSelect or add product:`, - { - chat_id: chatId, - message_id: messageId, - reply_markup: keyboard - } - ); - } catch (error) { - console.error('Error in handleCategorySelection:', error); - await bot.sendMessage(chatId, 'Error loading products. Please try again.'); - } - } - - static async handleProductListPage(callbackQuery) { - if (!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 bot.editMessageText(text, { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: markup, - parse_mode: 'HTML' - }); - } catch (e) { - return; - } - } - - static async handleAddProduct(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const [locationId, categoryId] = callbackQuery.data.replace('add_product_', '').split('_'); - - try { - 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 add product, send a JSON file with product in the following format:\n\n
${jsonExample}
\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; - - userStates.set(chatId, { - action: 'import_products', - locationId, - categoryId - }); - - await bot.editMessageText(message, { - chat_id: chatId, - message_id: messageId, - parse_mode: 'HTML', - reply_markup: { - inline_keyboard: [[ - { - text: '❌ Cancel', - callback_data: `prod_category_${locationId}_${categoryId}` - } - ]] - } - }); - } catch (error) { - console.error('Error in handleAddProduct:', error); - await bot.sendMessage(chatId, 'Error preparing product import. Please try again.'); - } - } - - static async handleProductImport(msg) { - const chatId = msg.chat.id; - const state = userStates.get(chatId); - - if (!state || state.action !== 'import_products') { - return false; - } - - if (!isAdmin(msg.from.id)) { - await bot.sendMessage(chatId, 'Unauthorized access.'); - return; - } - - try { - let products; - let jsonContent; - - // Handle file upload - if (msg.document) { - if (!msg.document.file_name.endsWith('.json')) { - await bot.sendMessage(chatId, 'Please upload a .json file.'); - return true; - } - - const file = await bot.getFile(msg.document.file_id); - - const fileContent = await bot.downloadFile(file.file_id, '.'); - jsonContent = await fs.readFile(fileContent, 'utf8'); - await fs.rm(fileContent); - - } else if (msg.text) { - jsonContent = msg.text; - } else { - await 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 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) { - if (!Validators.isValidString(product.name, 255)) { - await bot.sendMessage(chatId, `Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Ρ‚ΠΎΠ²Π°Ρ€Π° "${product.name}"`); - await db.runAsync('ROLLBACK'); - return true; - } - if (!Validators.isValidPrice(product.price)) { - await bot.sendMessage(chatId, `Ошибка: нСдопустимая Ρ†Π΅Π½Π° "${product.price}"`); - await db.runAsync('ROLLBACK'); - return true; - } - if (!Number.isFinite(product.quantity_in_stock) || product.quantity_in_stock < 0) { - await bot.sendMessage(chatId, `Ошибка: нСдопустимоС количСство "${product.quantity_in_stock}"`); - await db.runAsync('ROLLBACK'); - return true; - } - await db.runAsync( - `INSERT INTO products ( - location_id, category_id, - name, price, description, private_data, - quantity_in_stock, photo_url, hidden_photo_url, - hidden_coordinates, hidden_description - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - state.locationId, state.categoryId, - 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 bot.sendMessage( - chatId, - `βœ… Successfully imported ${products.length} products!`, - { - reply_markup: { - inline_keyboard: [[ - { - text: 'Β« Back to Products', - callback_data: `prod_category_${state.locationId}_${state.categoryId}` - } - ]] - } - } - ); - - userStates.delete(chatId); - } catch (error) { - console.error('Error importing products:', error); - await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); - await db.runAsync('ROLLBACK'); - } - - return true; - } - - static async handleProductEditImport(msg) { - const chatId = msg.chat.id; - const state = userStates.get(chatId); - - if (!state || state.action !== 'edit_product') { - return false; - } - - if (!isAdmin(msg.from.id)) { - await bot.sendMessage(chatId, 'Unauthorized access.'); - return; - } - - try { - let product; - let jsonContent; - - // Handle file upload - if (msg.document) { - if (!msg.document.file_name.endsWith('.json')) { - await bot.sendMessage(chatId, 'Please upload a .json file.'); - return true; - } - - const file = await bot.getFile(msg.document.file_id); - - const fileContent = await bot.downloadFile(file.file_id, '.'); - jsonContent = await fs.readFile(fileContent, 'utf8'); - await fs.rm(fileContent); - - } else if (msg.text) { - jsonContent = msg.text; - } else { - await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); - return true; - } - - try { - product = JSON.parse(jsonContent); - } catch (e) { - await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.'); - return true; - } - - await db.runAsync('BEGIN TRANSACTION'); - - if (!Validators.isValidString(product.name, 255)) { - await bot.sendMessage(chatId, 'Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Ρ‚ΠΎΠ²Π°Ρ€Π°'); - await db.runAsync('ROLLBACK'); - return true; - } - if (!Validators.isValidPrice(product.price)) { - await bot.sendMessage(chatId, 'Ошибка: нСдопустимая Ρ†Π΅Π½Π°'); - await db.runAsync('ROLLBACK'); - return true; - } - - await db.runAsync( - `UPDATE products SET - location_id = ?, - category_id = ?, - name = ?, - price = ?, - description = ?, - private_data = ?, - quantity_in_stock = ?, - photo_url = ?, - hidden_photo_url = ?, - hidden_coordinates = ?, - hidden_description = ? - WHERE - id = ? - `, - [ - state.locationId, state.categoryId, - 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.productId - ] - ); - - await db.runAsync('COMMIT'); - - await bot.sendMessage( - chatId, - `βœ… Successfully edited!`, - { - reply_markup: { - inline_keyboard: [[ - { - text: 'Β« Back to Products', - callback_data: `prod_category_${state.locationId}_${state.categoryId}` - } - ]] - } - } - ); - - userStates.delete(chatId); - } catch (error) { - console.error('Error importing products:', error); - await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); - await db.runAsync('ROLLBACK'); - } - - return true; - } - - static async handleViewProduct(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const productId = callbackQuery.data.replace('view_product_', ''); - - try { - const product = await ProductService.getDetailedProductById(productId); - - if (!product) { - throw new Error('Product not found'); - } - - const location = await LocationService.getLocationById(product.location_id); - - if (!location) { - throw new Error('Location not found'); - } - - const message = ` - πŸ“¦ Product Details: - - Name: ${product.name} - Price: $${product.price} - Description: ${product.description} - Stock: ${product.quantity_in_stock} - Location: ${location.country}, ${location.city}, ${location.district} - Category: ${product.category_name} - - πŸ”’ Private Information: - ${product.private_data} - 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_category_${product.location_id}_${product.category_id}` // Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ Π½Π° ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΡŽ - }] - ] - }; - - let photoMessage; - let hiddenPhotoMessage; - - // Send product photos - if (product.photo_url) { - try { - photoMessage = await bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'}); - } catch (e) { - photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'}) - } - } - if (product.hidden_photo_url) { - try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'}); - } catch (e) { - hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'}) - } - } - - userStates.set(chatId, { - msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id] - }) - - await bot.deleteMessage(chatId, messageId); - await bot.sendMessage(chatId, message, {reply_markup: keyboard}); - } catch (error) { - console.error('Error in handleViewProduct:', error); - await bot.sendMessage(chatId, 'Error loading product details. Please try again.'); - } - } - - static async handleProductEdit(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const chatId = callbackQuery.message.chat.id; - const messageId = callbackQuery.message.message_id; - const productId = callbackQuery.data.replace('edit_product_', ''); - - try { - const product = await ProductService.getDetailedProductById(productId); - - if (!product) { - throw new Error('Product not found'); - } - - const locationId = product.location_id; - const categoryId = product.category_id; - - const sampleProduct = { - name: product.name, - price: product.price, - description: product.description, - private_data: product.private_data, - quantity_in_stock: product.quantity_in_stock, - photo_url: product.photo_url, - hidden_photo_url: product.hidden_photo_url, - hidden_coordinates: product.hidden_coordinates, - hidden_description: product.hidden_description - }; - - const jsonExample = JSON.stringify(sampleProduct, null, 2); - const message = `To edit product, send a JSON file with product in the following format:\n\n
${jsonExample}
\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; - - userStates.set(chatId, { - action: 'edit_product', - locationId, - categoryId, - productId - }); - - await bot.editMessageText(message, { - chat_id: chatId, - message_id: messageId, - parse_mode: 'HTML', - reply_markup: { - inline_keyboard: [[ - { - text: '❌ Cancel', - callback_data: `prod_category_${locationId}_${categoryId}` // ВозвращаСмся ΠΊ списку Ρ‚ΠΎΠ²Π°Ρ€ΠΎΠ² Π² ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ - } - ]] - } - }); - } catch (error) { - console.error('Error in handleProductEdit:', error); - await bot.sendMessage(chatId, 'Error loading product details. Please try again.'); - } - } - - static async handleProductDelete(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const productId = callbackQuery.data.replace('delete_product_', ''); - const chatId = callbackQuery.message.chat.id; - - try { - const product = await ProductService.getDetailedProductById(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 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 bot.sendMessage(chatId, 'Error processing delete request. Please try again.'); - } - } - - static async handleConfirmDelete(callbackQuery) { - if (!isAdmin(callbackQuery.from.id)) { - return; - } - - const productId = callbackQuery.data.replace('confirm_delete_product_', ''); - const chatId = callbackQuery.message.chat.id; - - try { - const product = await ProductService.getDetailedProductById(productId); - - if (!product) { - throw new Error('Product not found'); - } - - const locationId = product.location_id; - const categoryId = product.category_id; - - 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:', e); - throw e; - } - - const keyboard = { - inline_keyboard: [ - [{ text: 'Β« Back', callback_data: `prod_category_${locationId}_${categoryId}` }] // ВозвращаСмся ΠΊ списку Ρ‚ΠΎΠ²Π°Ρ€ΠΎΠ² Π² ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ - ] - }; - - await 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 bot.sendMessage(chatId, 'Error deleting product. Please try again.'); - } - } -} \ No newline at end of file diff --git a/src/handlers/adminHandlers/product/categoryAddHandler.js b/src/handlers/adminHandlers/product/categoryAddHandler.js new file mode 100644 index 0000000..073848a --- /dev/null +++ b/src/handlers/adminHandlers/product/categoryAddHandler.js @@ -0,0 +1,91 @@ +import db from '../../../config/database.js'; +import { isAdmin } from '../../../middleware/auth.js'; +import LocationService from '../../../services/locationService.js'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; +import Validators from '../../../utils/validators.js'; + +export default class CategoryAddHandler { + + static async handleCategoryInput(msg) { + const chatId = msg.chat.id; + const state = userStates.get(chatId); + + if (!state || !state.action?.startsWith('add_category_')) { + return false; + } + + if (!isAdmin(msg.from.id)) { + await bot.sendMessage(chatId, 'Unauthorized access.'); + return; + } + + try { + const locationId = state.action.replace('add_category_', ''); + + if (!Validators.isValidString(msg.text, 255)) { + await bot.sendMessage(chatId, 'Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ'); + return true; + } + + await db.runAsync( + 'INSERT INTO categories (location_id, name) VALUES (?, ?)', + [locationId, msg.text] + ); + + const location = await LocationService.getLocationById(locationId); + + await 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}` + } + ]] + } + } + ); + + userStates.delete(chatId); + } catch (error) { + if (error.code === 'SQLITE_CONSTRAINT') { + await bot.sendMessage(chatId, 'This category already exists in this location.'); + } else { + console.error('Error adding category:', error); + await bot.sendMessage(chatId, 'Error adding category. Please try again.'); + } + } + + return true; + } + + static async handleAddCategory(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const locationId = callbackQuery.data.replace('add_category_', ''); + + userStates.set(chatId, {action: `add_category_${locationId}`}); + + const location = await LocationService.getLocationById(locationId); + + await 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_${location.country}_${location.city}_${location.district}`} + ]] + } + } + ); + } +} diff --git a/src/handlers/adminHandlers/product/categoryEditHandler.js b/src/handlers/adminHandlers/product/categoryEditHandler.js new file mode 100644 index 0000000..f3c9e6b --- /dev/null +++ b/src/handlers/adminHandlers/product/categoryEditHandler.js @@ -0,0 +1,82 @@ +import db from '../../../config/database.js'; +import { isAdmin } from '../../../middleware/auth.js'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; +import Validators from '../../../utils/validators.js'; + +export default class CategoryEditHandler { + + static async handleCategoryUpdate(msg) { + const chatId = msg.chat.id; + const state = userStates.get(chatId); + + if (!state || !state.action?.startsWith('edit_category_')) { + return false; + } + + if (!isAdmin(msg.from.id)) { + await bot.sendMessage(chatId, 'НСавторизованный доступ.'); + return; + } + + try { + const [locationId, categoryId] = state.action.replace('edit_category_', '').split('_'); + + if (!Validators.isValidString(msg.text, 255)) { + await bot.sendMessage(chatId, 'Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ'); + return true; + } + + await db.runAsync( + 'UPDATE categories SET name = ? WHERE id = ? AND location_id = ?', + [msg.text, categoryId, locationId] + ); + + await bot.sendMessage( + chatId, + `βœ… НазваниС ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΎ Π½Π° "${msg.text}".`, + { + reply_markup: { + inline_keyboard: [[ + { + text: 'Β« Назад ΠΊ катСгориям', + callback_data: `prod_category_${locationId}_${categoryId}` + } + ]] + } + } + ); + + userStates.delete(chatId); + } catch (error) { + console.error('Error updating category:', error); + await bot.sendMessage(chatId, 'Ошибка обновлСния ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ. ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΏΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ снова.'); + } + + return true; + } + + static async handleEditCategory(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const [locationId, categoryId] = callbackQuery.data.replace('edit_category_', '').split('_'); + + userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` }); + + await bot.editMessageText( + 'ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²Π²Π΅Π΄ΠΈΡ‚Π΅ Π½ΠΎΠ²ΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ:', + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: { + inline_keyboard: [[ + { text: '❌ ΠžΡ‚ΠΌΠ΅Π½Π°', callback_data: `prod_category_${locationId}_${categoryId}` } + ]] + } + } + ); + } +} diff --git a/src/handlers/adminHandlers/product/categorySelectionHandler.js b/src/handlers/adminHandlers/product/categorySelectionHandler.js new file mode 100644 index 0000000..5ee0498 --- /dev/null +++ b/src/handlers/adminHandlers/product/categorySelectionHandler.js @@ -0,0 +1,48 @@ +import { isAdmin } from '../../../middleware/auth.js'; +import LocationService from '../../../services/locationService.js'; +import bot from '../../../context/bot.js'; +import CategoryService from '../../../services/categoryService.js'; +import ProductService from '../../../services/productService.js'; + +export default class CategorySelectionHandler { + + static async handleCategorySelection(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_'); + + try { + const category = await CategoryService.getCategoryById(categoryId); + const location = await LocationService.getLocationById(locationId); + + const products = await ProductService.getProductsByCategoryId(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: 'βž• Add Product', callback_data: `add_product_${locationId}_${categoryId}` }], + [{ text: 'Β« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }] + ] + }; + + await bot.editMessageText( + `πŸ“¦ Category: ${category.name}\nSelect or add product:`, + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleCategorySelection:', error); + await bot.sendMessage(chatId, 'Error loading products. Please try again.'); + } + } +} diff --git a/src/handlers/adminHandlers/product/createHandler.js b/src/handlers/adminHandlers/product/createHandler.js new file mode 100644 index 0000000..1f7b786 --- /dev/null +++ b/src/handlers/adminHandlers/product/createHandler.js @@ -0,0 +1,56 @@ +import { isAdmin } from '../../../middleware/auth.js'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; + +export default class CreateHandler { + + static async handleAddProduct(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [locationId, categoryId] = callbackQuery.data.replace('add_product_', '').split('_'); + + try { + 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 add product, send a JSON file with product in the following format:\n\n
${jsonExample}
\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; + + userStates.set(chatId, { + action: 'import_products', + locationId, + categoryId + }); + + await bot.editMessageText(message, { + chat_id: chatId, + message_id: messageId, + parse_mode: 'HTML', + reply_markup: { + inline_keyboard: [[ + { + text: '❌ Cancel', + callback_data: `prod_category_${locationId}_${categoryId}` + } + ]] + } + }); + } catch (error) { + console.error('Error in handleAddProduct:', error); + await bot.sendMessage(chatId, 'Error preparing product import. Please try again.'); + } + } +} diff --git a/src/handlers/adminHandlers/product/deleteHandler.js b/src/handlers/adminHandlers/product/deleteHandler.js new file mode 100644 index 0000000..6d832f3 --- /dev/null +++ b/src/handlers/adminHandlers/product/deleteHandler.js @@ -0,0 +1,97 @@ +import db from '../../../config/database.js'; +import { isAdmin } from '../../../middleware/auth.js'; +import bot from '../../../context/bot.js'; +import ProductService from '../../../services/productService.js'; + +export default class DeleteHandler { + + static async handleProductDelete(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const productId = callbackQuery.data.replace('delete_product_', ''); + const chatId = callbackQuery.message.chat.id; + + try { + const product = await ProductService.getDetailedProductById(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_category_${product.location_id}_${product.category_id}` + } + ] + ] + }; + + await 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 bot.sendMessage(chatId, 'Error processing delete request. Please try again.'); + } + } + + static async handleConfirmDelete(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const productId = callbackQuery.data.replace('confirm_delete_product_', ''); + const chatId = callbackQuery.message.chat.id; + + try { + const product = await ProductService.getDetailedProductById(productId); + + if (!product) { + throw new Error('Product not found'); + } + + const locationId = product.location_id; + const categoryId = product.category_id; + + 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:', e); + throw e; + } + + const keyboard = { + inline_keyboard: [ + [{ text: 'Β« Back', callback_data: `prod_category_${locationId}_${categoryId}` }] + ] + }; + + await 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 bot.sendMessage(chatId, 'Error deleting product. Please try again.'); + } + } +} diff --git a/src/handlers/adminHandlers/product/districtHandler.js b/src/handlers/adminHandlers/product/districtHandler.js new file mode 100644 index 0000000..742c893 --- /dev/null +++ b/src/handlers/adminHandlers/product/districtHandler.js @@ -0,0 +1,89 @@ +import { isAdmin } from '../../../middleware/auth.js'; +import LocationService from '../../../services/locationService.js'; +import CategoryService from '../../../services/categoryService.js'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; + +export default class DistrictHandler { + + static async handleCitySelection(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + 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 LocationService.getDistrictsByCountryAndCity(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 bot.editMessageText( + `πŸ“ Select district in ${city}:`, + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleCitySelection:', error); + await bot.sendMessage(chatId, 'Error loading districts. Please try again.'); + } + } + + static async handleDistrictSelection(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_'); + + userStates.delete(chatId); + + try { + const location = await LocationService.getLocation(country, city, district); + + if (!location) { + throw new Error('Location not found'); + } + + const categories = await CategoryService.getCategoriesByLocationId(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 bot.editMessageText( + 'πŸ“¦ Select or add category:', + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleDistrictSelection:', error); + await bot.sendMessage(chatId, 'Error loading categories. Please try again.'); + } + } +} diff --git a/src/handlers/adminHandlers/product/editImportHandler.js b/src/handlers/adminHandlers/product/editImportHandler.js new file mode 100644 index 0000000..5a1c73d --- /dev/null +++ b/src/handlers/adminHandlers/product/editImportHandler.js @@ -0,0 +1,95 @@ +import db from '../../../config/database.js'; +import { isAdmin } from '../../../middleware/auth.js'; +import fs from 'fs/promises'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; +import { validateProductName, validateProductPrice } from './productValidator.js'; + +export default class EditImportHandler { + static async handleProductEditImport(msg) { + const chatId = msg.chat.id; + const state = userStates.get(chatId); + + if (!state || state.action !== 'edit_product') { + return false; + } + + if (!isAdmin(msg.from.id)) { + await bot.sendMessage(chatId, 'Unauthorized access.'); + return; + } + + try { + let product; + let jsonContent; + + if (msg.document) { + if (!msg.document.file_name.endsWith('.json')) { + await bot.sendMessage(chatId, 'Please upload a .json file.'); + return true; + } + + const file = await bot.getFile(msg.document.file_id); + const fileContent = await bot.downloadFile(file.file_id, '.'); + jsonContent = await fs.readFile(fileContent, 'utf8'); + await fs.rm(fileContent); + } else if (msg.text) { + jsonContent = msg.text; + } else { + await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); + return true; + } + + try { + product = JSON.parse(jsonContent); + } catch (e) { + await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.'); + return true; + } + + await db.runAsync('BEGIN TRANSACTION'); + + if (!validateProductName(product.name, chatId)) { + await db.runAsync('ROLLBACK'); + return true; + } + if (!validateProductPrice(product.price, chatId)) { + await db.runAsync('ROLLBACK'); + return true; + } + + await db.runAsync( + `UPDATE products SET + location_id = ?, category_id = ?, + name = ?, price = ?, description = ?, private_data = ?, + quantity_in_stock = ?, photo_url = ?, hidden_photo_url = ?, + hidden_coordinates = ?, hidden_description = ? + WHERE id = ?`, + [ + state.locationId, state.categoryId, + 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.productId + ] + ); + + await db.runAsync('COMMIT'); + + await bot.sendMessage(chatId, 'βœ… Successfully edited!', { + reply_markup: { + inline_keyboard: [[ + { text: 'Β« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` } + ]] + } + }); + + userStates.delete(chatId); + } catch (error) { + console.error('Error importing products:', error); + await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); + await db.runAsync('ROLLBACK'); + } + + return true; + } +} \ No newline at end of file diff --git a/src/handlers/adminHandlers/product/editStartHandler.js b/src/handlers/adminHandlers/product/editStartHandler.js new file mode 100644 index 0000000..fd22633 --- /dev/null +++ b/src/handlers/adminHandlers/product/editStartHandler.js @@ -0,0 +1,63 @@ +import { isAdmin } from '../../../middleware/auth.js'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; +import ProductService from '../../../services/productService.js'; + +export default class EditStartHandler { + static async handleProductEdit(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const productId = callbackQuery.data.replace('edit_product_', ''); + + try { + const product = await ProductService.getDetailedProductById(productId); + + if (!product) { + throw new Error('Product not found'); + } + + const locationId = product.location_id; + const categoryId = product.category_id; + + const sampleProduct = { + name: product.name, + price: product.price, + description: product.description, + private_data: product.private_data, + quantity_in_stock: product.quantity_in_stock, + photo_url: product.photo_url, + hidden_photo_url: product.hidden_photo_url, + hidden_coordinates: product.hidden_coordinates, + hidden_description: product.hidden_description + }; + + const jsonExample = JSON.stringify(sampleProduct, null, 2); + const message = `To edit product, send a JSON file with product data:\n\n
${jsonExample}
\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; + + userStates.set(chatId, { + action: 'edit_product', + locationId, + categoryId, + productId + }); + + await bot.editMessageText(message, { + chat_id: chatId, + message_id: messageId, + parse_mode: 'HTML', + reply_markup: { + inline_keyboard: [[ + { text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}` } + ]] + } + }); + } catch (error) { + console.error('Error in handleProductEdit:', error); + await bot.sendMessage(chatId, 'Error loading product details. Please try again.'); + } + } +} \ No newline at end of file diff --git a/src/handlers/adminHandlers/product/importHandler.js b/src/handlers/adminHandlers/product/importHandler.js new file mode 100644 index 0000000..3eae8f8 --- /dev/null +++ b/src/handlers/adminHandlers/product/importHandler.js @@ -0,0 +1,75 @@ +import db from '../../../config/database.js'; +import { isAdmin } from '../../../middleware/auth.js'; +import fs from 'fs/promises'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; +import ProductValidator from './productValidator.js'; + +export default class ImportHandler { + + static async handleProductImport(msg) { + const chatId = msg.chat.id; + const state = userStates.get(chatId); + if (!state || state.action !== 'import_products') return false; + if (!isAdmin(msg.from.id)) { + await bot.sendMessage(chatId, 'Unauthorized access.'); + return; + } + try { + const jsonContent = await this._extractJsonContent(msg, chatId); + if (!jsonContent) return true; + let products; + try { + products = JSON.parse(jsonContent); + if (!Array.isArray(products)) throw new Error('Input must be an array of products'); + } catch (e) { + await 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) { + const error = ProductValidator.validateProduct(product); + if (error) { + await bot.sendMessage(chatId, error); + await db.runAsync('ROLLBACK'); + return true; + } + await db.runAsync( + `INSERT INTO products (location_id, category_id, name, price, description, private_data, quantity_in_stock, photo_url, hidden_photo_url, hidden_coordinates, hidden_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [state.locationId, state.categoryId, 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 bot.sendMessage(chatId, `βœ… Successfully imported ${products.length} products!`, { + reply_markup: { + inline_keyboard: [[{ text: 'Β« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` }]] + } + }); + userStates.delete(chatId); + } catch (error) { + console.error('Error importing products:', error); + await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); + await db.runAsync('ROLLBACK'); + } + return true; + } + + static async _extractJsonContent(msg, chatId) { + if (msg.document) { + if (!msg.document.file_name.endsWith('.json')) { + await bot.sendMessage(chatId, 'Please upload a .json file.'); + return null; + } + const file = await bot.getFile(msg.document.file_id); + const fileContent = await bot.downloadFile(file.file_id, '.'); + const jsonContent = await fs.readFile(fileContent, 'utf8'); + await fs.rm(fileContent); + return jsonContent; + } + if (msg.text) { + return msg.text; + } + await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); + return null; + } +} diff --git a/src/handlers/adminHandlers/product/index.js b/src/handlers/adminHandlers/product/index.js new file mode 100644 index 0000000..d11158a --- /dev/null +++ b/src/handlers/adminHandlers/product/index.js @@ -0,0 +1,32 @@ +import CreateHandler from './createHandler.js'; +import ImportHandler from './importHandler.js'; +import EditStartHandler from './editStartHandler.js'; +import EditImportHandler from './editImportHandler.js'; +import DeleteHandler from './deleteHandler.js'; +import NavigationHandler from './navigationHandler.js'; +import DistrictHandler from './districtHandler.js'; +import CategoryAddHandler from './categoryAddHandler.js'; +import CategoryEditHandler from './categoryEditHandler.js'; +import CategorySelectionHandler from './categorySelectionHandler.js'; +import ViewHandler from './viewHandler.js'; +import ListHandler from './listHandler.js'; + +export default { + handleProductManagement: NavigationHandler.handleProductManagement, + handleCountrySelection: NavigationHandler.handleCountrySelection, + handleCitySelection: DistrictHandler.handleCitySelection, + handleDistrictSelection: DistrictHandler.handleDistrictSelection, + handleCategoryInput: CategoryAddHandler.handleCategoryInput, + handleAddCategory: CategoryAddHandler.handleAddCategory, + handleCategoryUpdate: CategoryEditHandler.handleCategoryUpdate, + handleEditCategory: CategoryEditHandler.handleEditCategory, + handleCategorySelection: CategorySelectionHandler.handleCategorySelection, + handleAddProduct: CreateHandler.handleAddProduct, + handleProductImport: ImportHandler.handleProductImport, + handleProductEdit: EditStartHandler.handleProductEdit, + handleProductEditImport: EditImportHandler.handleProductEditImport, + handleViewProduct: ViewHandler.handleViewProduct, + handleProductListPage: ListHandler.handleProductListPage, + handleProductDelete: DeleteHandler.handleProductDelete, + handleConfirmDelete: DeleteHandler.handleConfirmDelete, +}; \ No newline at end of file diff --git a/src/handlers/adminHandlers/product/listHandler.js b/src/handlers/adminHandlers/product/listHandler.js new file mode 100644 index 0000000..738dc18 --- /dev/null +++ b/src/handlers/adminHandlers/product/listHandler.js @@ -0,0 +1,90 @@ +import db from '../../../config/database.js'; +import { isAdmin } from '../../../middleware/auth.js'; +import bot from '../../../context/bot.js'; + +export default class ListHandler { + + static async viewProductsPage(locationId, categoryId, 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 = ? + ORDER BY name + LIMIT ? OFFSET ?`, + [locationId, categoryId, limit, offset] + ); + + if (products.length === 0 && page === 0) { + return { + text: 'No products for this location', + markup: { + inline_keyboard: [ + [{ text: 'πŸ“₯ Import Products', callback_data: `add_product_${locationId}_${categoryId}` }], + [{ text: 'Β« Back', callback_data: `prod_category_${locationId}_${categoryId}` }] + ] + } + }; + } + + if (products.length === 0 && page > 0) { + return await this.viewProductsPage(locationId, categoryId, previousPage); + } + + 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}` }] + ] + }; + + keyboard.inline_keyboard.push([ + { text: 'Β«', callback_data: `list_products_${locationId}_${categoryId}_${previousPage}` }, + { text: 'Β»', callback_data: `list_products_${locationId}_${categoryId}_${nextPage}` } + ]); + + keyboard.inline_keyboard.push([ + { text: 'Β« Back', callback_data: `prod_category_${locationId}_${categoryId}` } + ]); + + return { + text: `πŸ“¦ ${category.name}\nSelect product or import new ones:`, + markup: keyboard + }; + } catch (error) { + console.error('Error in viewProductsPage:', error); + return { text: 'Error loading products. Please try again.' }; + } + } + + static async handleProductListPage(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const [locationId, categoryId, page] = callbackQuery.data.replace('list_products_', '').split('_'); + + try { + const { text, markup } = await this.viewProductsPage(locationId, categoryId, parseInt(page)); + await bot.editMessageText(text, { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: markup, + parse_mode: 'HTML' + }); + } catch (e) { + return; + } + } +} diff --git a/src/handlers/adminHandlers/product/navigationHandler.js b/src/handlers/adminHandlers/product/navigationHandler.js new file mode 100644 index 0000000..4628f95 --- /dev/null +++ b/src/handlers/adminHandlers/product/navigationHandler.js @@ -0,0 +1,86 @@ +import { isAdmin } from '../../../middleware/auth.js'; +import LocationService from '../../../services/locationService.js'; +import bot from '../../../context/bot.js'; + +export default class NavigationHandler { + + static async handleProductManagement(msg) { + const chatId = msg.chat?.id || msg.message?.chat.id; + + if (!isAdmin(msg.from?.id || msg.message?.from.id)) { + await bot.sendMessage(chatId, 'Unauthorized access.'); + return; + } + + try { + const countries = await LocationService.getCountries() + + if (countries.length === 0) { + await 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 bot.sendMessage( + chatId, + '🌍 Select country to manage products:', + {reply_markup: keyboard} + ); + } catch (error) { + console.error('Error in handleProductManagement:', error); + await bot.sendMessage(chatId, 'Error loading locations. Please try again.'); + } + } + + static async handleCountrySelection(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const country = callbackQuery.data.replace('prod_country_', ''); + + try { + const cities = await LocationService.getCitiesByCountry(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 bot.editMessageText( + `πŸ™ Select city in ${country}:`, + { + chat_id: chatId, + message_id: messageId, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleCountrySelection:', error); + await bot.sendMessage(chatId, 'Error loading cities. Please try again.'); + } + } +} diff --git a/src/handlers/adminHandlers/product/productValidator.js b/src/handlers/adminHandlers/product/productValidator.js new file mode 100644 index 0000000..fdb2930 --- /dev/null +++ b/src/handlers/adminHandlers/product/productValidator.js @@ -0,0 +1,17 @@ +import Validators from '../../../utils/validators.js'; + +export default class ProductValidator { + + static validateProduct(product) { + if (!Validators.isValidString(product.name, 255)) { + return `Ошибка: нСдопустимоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Ρ‚ΠΎΠ²Π°Ρ€Π° "${product.name}"`; + } + if (!Validators.isValidPrice(product.price)) { + return `Ошибка: нСдопустимая Ρ†Π΅Π½Π° "${product.price}"`; + } + if (!Number.isFinite(product.quantity_in_stock) || product.quantity_in_stock < 0) { + return `Ошибка: нСдопустимоС количСство "${product.quantity_in_stock}"`; + } + return null; + } +} diff --git a/src/handlers/adminHandlers/product/viewHandler.js b/src/handlers/adminHandlers/product/viewHandler.js new file mode 100644 index 0000000..a1f5b27 --- /dev/null +++ b/src/handlers/adminHandlers/product/viewHandler.js @@ -0,0 +1,89 @@ +import { isAdmin } from '../../../middleware/auth.js'; +import bot from '../../../context/bot.js'; +import userStates from '../../../context/userStates.js'; +import LocationService from '../../../services/locationService.js'; +import ProductService from '../../../services/productService.js'; + +export default class ViewHandler { + + static async handleViewProduct(callbackQuery) { + if (!isAdmin(callbackQuery.from.id)) { + return; + } + + const chatId = callbackQuery.message.chat.id; + const messageId = callbackQuery.message.message_id; + const productId = callbackQuery.data.replace('view_product_', ''); + + try { + const product = await ProductService.getDetailedProductById(productId); + + if (!product) { + throw new Error('Product not found'); + } + + const location = await LocationService.getLocationById(product.location_id); + + if (!location) { + throw new Error('Location not found'); + } + + const message = ` + πŸ“¦ Product Details: + + Name: ${product.name} + Price: $${product.price} + Description: ${product.description} + Stock: ${product.quantity_in_stock} + Location: ${location.country}, ${location.city}, ${location.district} + Category: ${product.category_name} + + πŸ”’ Private Information: + ${product.private_data} + 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_category_${product.location_id}_${product.category_id}` + }] + ] + }; + + let photoMessage; + let hiddenPhotoMessage; + + if (product.photo_url) { + try { + photoMessage = await bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'}); + } catch (e) { + photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'}) + } + } + if (product.hidden_photo_url) { + try { + hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'}); + } catch (e) { + hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'}) + } + } + + userStates.set(chatId, { + msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id] + }) + + await bot.deleteMessage(chatId, messageId); + await bot.sendMessage(chatId, message, {reply_markup: keyboard}); + } catch (error) { + console.error('Error in handleViewProduct:', error); + await bot.sendMessage(chatId, 'Error loading product details. Please try again.'); + } + } +} diff --git a/src/index.js b/src/index.js index 20d8147..b8a3e96 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,7 @@ import adminHandler from "./handlers/adminHandlers/adminHandler.js"; import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocationHandler.js"; import adminDumpHandler from "./handlers/adminHandlers/adminDumpHandler.js"; import adminLocationHandler from "./handlers/adminHandlers/adminLocationHandler.js"; -import adminProductHandler from "./handlers/adminHandlers/adminProductHandler.js"; +import productHandler from "./handlers/adminHandlers/product/index.js"; import adminWalletsHandler from "./handlers/adminHandlers/adminWalletsHandler.js"; // Debug logging function @@ -70,17 +70,17 @@ bot.on('message', async (msg) => { } // Check for admin category input - if (await adminProductHandler.handleCategoryInput(msg)) { + if (await productHandler.handleCategoryInput(msg)) { return; } // Check for product import - if (await adminProductHandler.handleProductImport(msg)) { + if (await productHandler.handleProductImport(msg)) { return; } // Check for product edition - if (await adminProductHandler.handleProductEditImport(msg)) { + if (await productHandler.handleProductEditImport(msg)) { return; } @@ -95,7 +95,7 @@ bot.on('message', async (msg) => { } // Check for category update input - if (await adminProductHandler.handleCategoryUpdate(msg)) { + if (await productHandler.handleCategoryUpdate(msg)) { return; } @@ -116,7 +116,7 @@ bot.on('message', async (msg) => { break; case 'πŸ“¦ Manage Products': if (adminHandler.isAdmin(msg.from.id)) { - await adminProductHandler.handleProductManagement(msg); + await productHandler.handleProductManagement(msg); } break; case 'πŸ‘₯ Manage Users': @@ -271,43 +271,43 @@ bot.on('callback_query', async (callbackQuery) => { // Admin product management else if (action === 'manage_products') { logDebug(action, 'handleProductManagement'); - await adminProductHandler.handleProductManagement(callbackQuery); + await productHandler.handleProductManagement(callbackQuery); } else if (action.startsWith('prod_country_')) { logDebug(action, 'handleCountrySelection'); - await adminProductHandler.handleCountrySelection(callbackQuery); + await productHandler.handleCountrySelection(callbackQuery); } else if (action.startsWith('prod_city_')) { logDebug(action, 'handleCitySelection'); - await adminProductHandler.handleCitySelection(callbackQuery); + await productHandler.handleCitySelection(callbackQuery); } else if (action.startsWith('prod_district_')) { logDebug(action, 'handleDistrictSelection'); - await adminProductHandler.handleDistrictSelection(callbackQuery); + await productHandler.handleDistrictSelection(callbackQuery); } else if (action.startsWith('add_category_')) { logDebug(action, 'handleAddCategory'); - await adminProductHandler.handleAddCategory(callbackQuery); + await productHandler.handleAddCategory(callbackQuery); } else if (action.startsWith('edit_category_')) { logDebug(action, 'handleEditCategory'); - await adminProductHandler.handleEditCategory(callbackQuery); + await productHandler.handleEditCategory(callbackQuery); } else if (action.startsWith('prod_category_')) { logDebug(action, 'handleCategorySelection'); - await adminProductHandler.handleCategorySelection(callbackQuery); + await productHandler.handleCategorySelection(callbackQuery); } else if (action.startsWith('list_products_')) { logDebug(action, 'handleProductListPage'); - await adminProductHandler.handleProductListPage(callbackQuery); + await productHandler.handleProductListPage(callbackQuery); } else if (action.startsWith('add_product_')) { logDebug(action, 'handleAddProduct'); - await adminProductHandler.handleAddProduct(callbackQuery); + await productHandler.handleAddProduct(callbackQuery); } else if (action.startsWith('view_product_')) { logDebug(action, 'handleViewProduct'); - await adminProductHandler.handleViewProduct(callbackQuery); + await productHandler.handleViewProduct(callbackQuery); } else if (action.startsWith('edit_product_')) { logDebug(action, 'handleProductEdit'); - await adminProductHandler.handleProductEdit(callbackQuery) + await productHandler.handleProductEdit(callbackQuery) } else if (action.startsWith('delete_product_')) { logDebug(action, 'handleProductDelete'); - await adminProductHandler.handleProductDelete(callbackQuery); + await productHandler.handleProductDelete(callbackQuery); } else if (action.startsWith('confirm_delete_product_')) { logDebug(action, 'handleConfirmDelete'); - await adminProductHandler.handleConfirmDelete(callbackQuery); + await productHandler.handleConfirmDelete(callbackQuery); } // Admin user management else if (action.startsWith('view_user_')) {