refactor(arch): split adminProductHandler.js into 13 modular files (#51)

- 1093-line monolith → 13 files (all ≤97 lines)
- navigationHandler: product management entry + country selection
- districtHandler: city + district selection
- categoryAddHandler: add category input + handler
- categoryEditHandler: edit category input + handler
- categorySelectionHandler: category selection display
- createHandler: add product prompt
- importHandler: product import (JSON/text/file)
- editStartHandler: product edit prompt
- editImportHandler: product edit import
- deleteHandler: product delete + confirm
- viewHandler: product detail view
- listHandler: product list with pagination
- productValidator: shared validation utilities
- index.js: router re-exporting all 17 handler methods
- Removed duplicate handleCategorySelection (subcategories table doesn't exist)
- Removed handleSubcategoryInput/handleAddSubcategory (references non-existent subcategories table)
This commit is contained in:
NW
2026-06-17 22:41:04 +01:00
parent 4b8144ac40
commit 4b7ed0c251
16 changed files with 1029 additions and 1112 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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}`}
]]
}
}
);
}
}

View File

@@ -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}` }
]]
}
}
);
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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<pre>${jsonExample}</pre>\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.');
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<pre>${jsonExample}</pre>\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.');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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,
};

View File

@@ -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;
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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.');
}
}
}

View File

@@ -15,7 +15,7 @@ import adminHandler from "./handlers/adminHandlers/adminHandler.js";
import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocationHandler.js"; import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocationHandler.js";
import adminDumpHandler from "./handlers/adminHandlers/adminDumpHandler.js"; import adminDumpHandler from "./handlers/adminHandlers/adminDumpHandler.js";
import adminLocationHandler from "./handlers/adminHandlers/adminLocationHandler.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"; import adminWalletsHandler from "./handlers/adminHandlers/adminWalletsHandler.js";
// Debug logging function // Debug logging function
@@ -70,17 +70,17 @@ bot.on('message', async (msg) => {
} }
// Check for admin category input // Check for admin category input
if (await adminProductHandler.handleCategoryInput(msg)) { if (await productHandler.handleCategoryInput(msg)) {
return; return;
} }
// Check for product import // Check for product import
if (await adminProductHandler.handleProductImport(msg)) { if (await productHandler.handleProductImport(msg)) {
return; return;
} }
// Check for product edition // Check for product edition
if (await adminProductHandler.handleProductEditImport(msg)) { if (await productHandler.handleProductEditImport(msg)) {
return; return;
} }
@@ -95,7 +95,7 @@ bot.on('message', async (msg) => {
} }
// Check for category update input // Check for category update input
if (await adminProductHandler.handleCategoryUpdate(msg)) { if (await productHandler.handleCategoryUpdate(msg)) {
return; return;
} }
@@ -116,7 +116,7 @@ bot.on('message', async (msg) => {
break; break;
case '📦 Manage Products': case '📦 Manage Products':
if (adminHandler.isAdmin(msg.from.id)) { if (adminHandler.isAdmin(msg.from.id)) {
await adminProductHandler.handleProductManagement(msg); await productHandler.handleProductManagement(msg);
} }
break; break;
case '👥 Manage Users': case '👥 Manage Users':
@@ -271,43 +271,43 @@ bot.on('callback_query', async (callbackQuery) => {
// Admin product management // Admin product management
else if (action === 'manage_products') { else if (action === 'manage_products') {
logDebug(action, 'handleProductManagement'); logDebug(action, 'handleProductManagement');
await adminProductHandler.handleProductManagement(callbackQuery); await productHandler.handleProductManagement(callbackQuery);
} else if (action.startsWith('prod_country_')) { } else if (action.startsWith('prod_country_')) {
logDebug(action, 'handleCountrySelection'); logDebug(action, 'handleCountrySelection');
await adminProductHandler.handleCountrySelection(callbackQuery); await productHandler.handleCountrySelection(callbackQuery);
} else if (action.startsWith('prod_city_')) { } else if (action.startsWith('prod_city_')) {
logDebug(action, 'handleCitySelection'); logDebug(action, 'handleCitySelection');
await adminProductHandler.handleCitySelection(callbackQuery); await productHandler.handleCitySelection(callbackQuery);
} else if (action.startsWith('prod_district_')) { } else if (action.startsWith('prod_district_')) {
logDebug(action, 'handleDistrictSelection'); logDebug(action, 'handleDistrictSelection');
await adminProductHandler.handleDistrictSelection(callbackQuery); await productHandler.handleDistrictSelection(callbackQuery);
} else if (action.startsWith('add_category_')) { } else if (action.startsWith('add_category_')) {
logDebug(action, 'handleAddCategory'); logDebug(action, 'handleAddCategory');
await adminProductHandler.handleAddCategory(callbackQuery); await productHandler.handleAddCategory(callbackQuery);
} else if (action.startsWith('edit_category_')) { } else if (action.startsWith('edit_category_')) {
logDebug(action, 'handleEditCategory'); logDebug(action, 'handleEditCategory');
await adminProductHandler.handleEditCategory(callbackQuery); await productHandler.handleEditCategory(callbackQuery);
} else if (action.startsWith('prod_category_')) { } else if (action.startsWith('prod_category_')) {
logDebug(action, 'handleCategorySelection'); logDebug(action, 'handleCategorySelection');
await adminProductHandler.handleCategorySelection(callbackQuery); await productHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('list_products_')) { } else if (action.startsWith('list_products_')) {
logDebug(action, 'handleProductListPage'); logDebug(action, 'handleProductListPage');
await adminProductHandler.handleProductListPage(callbackQuery); await productHandler.handleProductListPage(callbackQuery);
} else if (action.startsWith('add_product_')) { } else if (action.startsWith('add_product_')) {
logDebug(action, 'handleAddProduct'); logDebug(action, 'handleAddProduct');
await adminProductHandler.handleAddProduct(callbackQuery); await productHandler.handleAddProduct(callbackQuery);
} else if (action.startsWith('view_product_')) { } else if (action.startsWith('view_product_')) {
logDebug(action, 'handleViewProduct'); logDebug(action, 'handleViewProduct');
await adminProductHandler.handleViewProduct(callbackQuery); await productHandler.handleViewProduct(callbackQuery);
} else if (action.startsWith('edit_product_')) { } else if (action.startsWith('edit_product_')) {
logDebug(action, 'handleProductEdit'); logDebug(action, 'handleProductEdit');
await adminProductHandler.handleProductEdit(callbackQuery) await productHandler.handleProductEdit(callbackQuery)
} else if (action.startsWith('delete_product_')) { } else if (action.startsWith('delete_product_')) {
logDebug(action, 'handleProductDelete'); logDebug(action, 'handleProductDelete');
await adminProductHandler.handleProductDelete(callbackQuery); await productHandler.handleProductDelete(callbackQuery);
} else if (action.startsWith('confirm_delete_product_')) { } else if (action.startsWith('confirm_delete_product_')) {
logDebug(action, 'handleConfirmDelete'); logDebug(action, 'handleConfirmDelete');
await adminProductHandler.handleConfirmDelete(callbackQuery); await productHandler.handleConfirmDelete(callbackQuery);
} }
// Admin user management // Admin user management
else if (action.startsWith('view_user_')) { else if (action.startsWith('view_user_')) {