Delet Subcategory Function

This commit is contained in:
NW 2024-12-13 16:41:41 +00:00
parent 3e78e231f3
commit 95d5fe644d
4 changed files with 121 additions and 140 deletions

View File

@ -165,7 +165,6 @@ const initDb = async () => {
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL, location_id INTEGER NOT NULL,
category_id INTEGER NOT NULL, category_id INTEGER NOT NULL,
subcategory_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
private_data TEXT, private_data TEXT,
@ -177,8 +176,7 @@ const initDb = async () => {
hidden_description TEXT, hidden_description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE, FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
FOREIGN KEY (subcategory_id) REFERENCES subcategories(id) ON DELETE CASCADE
) )
`); `);
@ -222,18 +220,6 @@ const initDb = async () => {
) )
`); `);
// Create subcategories table
await db.runAsync(`
CREATE TABLE IF NOT EXISTS subcategories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE(category_id, name)
)
`);
// Commit transaction // Commit transaction
await db.runAsync('COMMIT'); await db.runAsync('COMMIT');
console.log('Database tables initialized successfully'); console.log('Database tables initialized successfully');

View File

@ -327,33 +327,31 @@ export default class AdminProductHandler {
if (!this.isAdmin(callbackQuery.from.id)) { if (!this.isAdmin(callbackQuery.from.id)) {
return; return;
} }
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_'); const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
try { try {
const subcategories = await CategoryService.getSubcategoriesByCategoryId(categoryId);
const category = await CategoryService.getCategoryById(categoryId); const category = await CategoryService.getCategoryById(categoryId);
const location = await LocationService.getLocationById(locationId); const location = await LocationService.getLocationById(locationId);
// Получаем товары для выбранной категории
const products = await ProductService.getProductsByCategoryId(categoryId);
const keyboard = { const keyboard = {
inline_keyboard: [ inline_keyboard: [
...subcategories.map(sub => [{ ...products.map(prod => [{
text: sub.name, text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}` callback_data: `view_product_${prod.id}`
}]), }]),
[{text: ' Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}`}], [{ text: ' Add Product', callback_data: `add_product_${locationId}_${categoryId}` }],
[{text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}`}], [{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }]
[{
text: '« Back',
callback_data: `prod_district_${location.country}_${location.city}_${location.district}`
}]
] ]
}; };
await bot.editMessageText( await bot.editMessageText(
`📦 Category: ${category.name}\nSelect or add subcategory:`, `📦 Category: ${category.name}\nSelect or add product:`,
{ {
chat_id: chatId, chat_id: chatId,
message_id: messageId, message_id: messageId,
@ -362,7 +360,7 @@ export default class AdminProductHandler {
); );
} catch (error) { } catch (error) {
console.error('Error in handleCategorySelection:', error); console.error('Error in handleCategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading subcategories. Please try again.'); await bot.sendMessage(chatId, 'Error loading products. Please try again.');
} }
} }
@ -517,40 +515,43 @@ export default class AdminProductHandler {
} }
} }
static async handleSubcategorySelection(callbackQuery) { static async handleCategorySelection(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) { if (!this.isAdmin(callbackQuery.from.id)) {
return; return;
} }
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_'); const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
const state = userStates.get(chatId)
for (const msgId of state?.msgToDelete || []) {
try {
await bot.deleteMessage(chatId, msgId);
} catch (e) {
// ignore if can't delete
}
}
userStates.delete(chatId);
try { try {
const {text, markup} = await this.viewProductsPage(locationId, categoryId, subcategoryId, 0); 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( await bot.editMessageText(
text, `📦 Category: ${category.name}\nSelect or add product:`,
{ {
chat_id: chatId, chat_id: chatId,
message_id: messageId, message_id: messageId,
reply_markup: markup reply_markup: keyboard
} }
); );
} catch (error) { } catch (error) {
console.error('Error in handleSubcategorySelection:', error); console.error('Error in handleCategorySelection:', error);
await bot.sendMessage(chatId, 'Error loading products. Please try again.'); await bot.sendMessage(chatId, 'Error loading products. Please try again.');
} }
} }
@ -581,11 +582,11 @@ export default class AdminProductHandler {
if (!this.isAdmin(callbackQuery.from.id)) { if (!this.isAdmin(callbackQuery.from.id)) {
return; return;
} }
const chatId = callbackQuery.message.chat.id; const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id; const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_'); const [locationId, categoryId] = callbackQuery.data.replace('add_product_', '').split('_');
try { try {
const sampleProducts = [ const sampleProducts = [
{ {
@ -600,17 +601,16 @@ export default class AdminProductHandler {
hidden_description: "Secret location details" hidden_description: "Secret location details"
} }
]; ];
const jsonExample = JSON.stringify(sampleProducts, null, 2); const jsonExample = JSON.stringify(sampleProducts, null, 2);
const message = `To import products, send a JSON file with an array of products in the following format:\n\n<pre>${jsonExample}</pre>\n\nEach product must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`; 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, { userStates.set(chatId, {
action: 'import_products', action: 'import_products',
locationId, locationId,
categoryId, categoryId
subcategoryId
}); });
await bot.editMessageText(message, { await bot.editMessageText(message, {
chat_id: chatId, chat_id: chatId,
message_id: messageId, message_id: messageId,
@ -619,7 +619,7 @@ export default class AdminProductHandler {
inline_keyboard: [[ inline_keyboard: [[
{ {
text: '❌ Cancel', text: '❌ Cancel',
callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` callback_data: `prod_category_${locationId}_${categoryId}`
} }
]] ]]
} }
@ -633,40 +633,40 @@ export default class AdminProductHandler {
static async handleProductImport(msg) { static async handleProductImport(msg) {
const chatId = msg.chat.id; const chatId = msg.chat.id;
const state = userStates.get(chatId); const state = userStates.get(chatId);
if (!state || state.action !== 'import_products') { if (!state || state.action !== 'import_products') {
return false; return false;
} }
if (!this.isAdmin(msg.from.id)) { if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.'); await bot.sendMessage(chatId, 'Unauthorized access.');
return; return;
} }
try { try {
let products; let products;
let jsonContent; let jsonContent;
// Handle file upload // Handle file upload
if (msg.document) { if (msg.document) {
if (!msg.document.file_name.endsWith('.json')) { if (!msg.document.file_name.endsWith('.json')) {
await bot.sendMessage(chatId, 'Please upload a .json file.'); await bot.sendMessage(chatId, 'Please upload a .json file.');
return true; return true;
} }
const file = await bot.getFile(msg.document.file_id); const file = await bot.getFile(msg.document.file_id);
const fileContent = await bot.downloadFile(file.file_id, '.'); const fileContent = await bot.downloadFile(file.file_id, '.');
jsonContent = await fs.readFile(fileContent, 'utf8'); jsonContent = await fs.readFile(fileContent, 'utf8');
await fs.rm(fileContent); await fs.rm(fileContent);
} else if (msg.text) { } else if (msg.text) {
jsonContent = msg.text; jsonContent = msg.text;
} else { } else {
await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
return true; return true;
} }
try { try {
products = JSON.parse(jsonContent); products = JSON.parse(jsonContent);
if (!Array.isArray(products)) { if (!Array.isArray(products)) {
@ -676,28 +676,28 @@ export default class AdminProductHandler {
await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.'); await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
return true; return true;
} }
await db.runAsync('BEGIN TRANSACTION'); await db.runAsync('BEGIN TRANSACTION');
for (const product of products) { for (const product of products) {
await db.runAsync( await db.runAsync(
`INSERT INTO products ( `INSERT INTO products (
location_id, category_id, subcategory_id, location_id, category_id,
name, price, description, private_data, name, price, description, private_data,
quantity_in_stock, photo_url, hidden_photo_url, quantity_in_stock, photo_url, hidden_photo_url,
hidden_coordinates, hidden_description hidden_coordinates, hidden_description
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
state.locationId, state.categoryId, state.subcategoryId, state.locationId, state.categoryId,
product.name, product.price, product.description, product.private_data, product.name, product.price, product.description, product.private_data,
product.quantity_in_stock, product.photo_url, product.hidden_photo_url, product.quantity_in_stock, product.photo_url, product.hidden_photo_url,
product.hidden_coordinates, product.hidden_description product.hidden_coordinates, product.hidden_description
] ]
); );
} }
await db.runAsync('COMMIT'); await db.runAsync('COMMIT');
await bot.sendMessage( await bot.sendMessage(
chatId, chatId,
`✅ Successfully imported ${products.length} products!`, `✅ Successfully imported ${products.length} products!`,
@ -706,96 +706,95 @@ export default class AdminProductHandler {
inline_keyboard: [[ inline_keyboard: [[
{ {
text: '« Back to Products', text: '« Back to Products',
callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` callback_data: `prod_category_${state.locationId}_${state.categoryId}`
} }
]] ]]
} }
} }
); );
userStates.delete(chatId); userStates.delete(chatId);
} catch (error) { } catch (error) {
console.error('Error importing products:', error); console.error('Error importing products:', error);
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
await db.runAsync('ROLLBACK'); await db.runAsync('ROLLBACK');
} }
return true; return true;
} }
static async handleProductEditImport(msg) { static async handleProductEditImport(msg) {
const chatId = msg.chat.id; const chatId = msg.chat.id;
const state = userStates.get(chatId); const state = userStates.get(chatId);
if (!state || state.action !== 'edit_product') { if (!state || state.action !== 'edit_product') {
return false; return false;
} }
if (!this.isAdmin(msg.from.id)) { if (!this.isAdmin(msg.from.id)) {
await bot.sendMessage(chatId, 'Unauthorized access.'); await bot.sendMessage(chatId, 'Unauthorized access.');
return; return;
} }
try { try {
let product; let product;
let jsonContent; let jsonContent;
// Handle file upload // Handle file upload
if (msg.document) { if (msg.document) {
if (!msg.document.file_name.endsWith('.json')) { if (!msg.document.file_name.endsWith('.json')) {
await bot.sendMessage(chatId, 'Please upload a .json file.'); await bot.sendMessage(chatId, 'Please upload a .json file.');
return true; return true;
} }
const file = await bot.getFile(msg.document.file_id); const file = await bot.getFile(msg.document.file_id);
const fileContent = await bot.downloadFile(file.file_id, '.'); const fileContent = await bot.downloadFile(file.file_id, '.');
jsonContent = await fs.readFile(fileContent, 'utf8'); jsonContent = await fs.readFile(fileContent, 'utf8');
await fs.rm(fileContent); await fs.rm(fileContent);
} else if (msg.text) { } else if (msg.text) {
jsonContent = msg.text; jsonContent = msg.text;
} else { } else {
await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.'); await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
return true; return true;
} }
try { try {
product = JSON.parse(jsonContent); product = JSON.parse(jsonContent);
} catch (e) { } catch (e) {
await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.'); await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
return true; return true;
} }
await db.runAsync('BEGIN TRANSACTION'); await db.runAsync('BEGIN TRANSACTION');
await db.runAsync( await db.runAsync(
`UPDATE products SET `UPDATE products SET
location_id = ?, location_id = ?,
category_id = ?, category_id = ?,
subcategory_id = ?, name = ?,
name = ?, price = ?,
price = ?, description = ?,
description = ?, private_data = ?,
private_data = ?, quantity_in_stock = ?,
quantity_in_stock = ?, photo_url = ?,
photo_url = ?, hidden_photo_url = ?,
hidden_photo_url = ?, hidden_coordinates = ?,
hidden_coordinates = ?, hidden_description = ?
hidden_description = ? WHERE
WHERE id = ?
id = ? `,
`,
[ [
state.locationId, state.categoryId, state.subcategoryId, state.locationId, state.categoryId,
product.name, product.price, product.description, product.private_data, product.name, product.price, product.description, product.private_data,
product.quantity_in_stock, product.photo_url, product.hidden_photo_url, product.quantity_in_stock, product.photo_url, product.hidden_photo_url,
product.hidden_coordinates, product.hidden_description, state.productId product.hidden_coordinates, product.hidden_description, state.productId
] ]
); );
await db.runAsync('COMMIT'); await db.runAsync('COMMIT');
await bot.sendMessage( await bot.sendMessage(
chatId, chatId,
`✅ Successfully edited!`, `✅ Successfully edited!`,
@ -804,20 +803,20 @@ export default class AdminProductHandler {
inline_keyboard: [[ inline_keyboard: [[
{ {
text: '« Back to Products', text: '« Back to Products',
callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` callback_data: `prod_category_${state.locationId}_${state.categoryId}`
} }
]] ]]
} }
} }
); );
userStates.delete(chatId); userStates.delete(chatId);
} catch (error) { } catch (error) {
console.error('Error importing products:', error); console.error('Error importing products:', error);
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.'); await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
await db.runAsync('ROLLBACK'); await db.runAsync('ROLLBACK');
} }
return true; return true;
} }

View File

@ -69,11 +69,6 @@ bot.on('message', async (msg) => {
return; return;
} }
// Check for admin subcategory input
if (await adminProductHandler.handleSubcategoryInput(msg)) {
return;
}
// Check for product import // Check for product import
if (await adminProductHandler.handleProductImport(msg)) { if (await adminProductHandler.handleProductImport(msg)) {
return; return;
@ -217,9 +212,6 @@ bot.on('callback_query', async (callbackQuery) => {
} else if (action.startsWith('shop_category_')) { } else if (action.startsWith('shop_category_')) {
logDebug(action, 'handleCategorySelection'); logDebug(action, 'handleCategorySelection');
await userProductHandler.handleCategorySelection(callbackQuery); await userProductHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('shop_subcategory_')) {
logDebug(action, 'handleSubcategorySelection');
await userProductHandler.handleSubcategorySelection(callbackQuery);
} else if (action.startsWith('shop_product_')) { } else if (action.startsWith('shop_product_')) {
logDebug(action, 'handleProductSelection'); logDebug(action, 'handleProductSelection');
await userProductHandler.handleProductSelection(callbackQuery); await userProductHandler.handleProductSelection(callbackQuery);
@ -282,12 +274,6 @@ bot.on('callback_query', async (callbackQuery) => {
} else if (action.startsWith('prod_category_')) { } else if (action.startsWith('prod_category_')) {
logDebug(action, 'handleCategorySelection'); logDebug(action, 'handleCategorySelection');
await adminProductHandler.handleCategorySelection(callbackQuery); await adminProductHandler.handleCategorySelection(callbackQuery);
} else if (action.startsWith('add_subcategory_')) {
logDebug(action, 'handleAddSubcategory');
await adminProductHandler.handleAddSubcategory(callbackQuery);
} else if (action.startsWith('prod_subcategory_')) {
logDebug(action, 'handleSubcategorySelection');
await adminProductHandler.handleSubcategorySelection(callbackQuery);
} else if (action.startsWith('list_products_')) { } else if (action.startsWith('list_products_')) {
logDebug(action, 'handleProductListPage'); logDebug(action, 'handleProductListPage');
await adminProductHandler.handleProductListPage(callbackQuery); await adminProductHandler.handleProductListPage(callbackQuery);

View File

@ -12,26 +12,36 @@ class ProductService {
static async getDetailedProductById(productId) { static async getDetailedProductById(productId) {
return await db.getAsync( return await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name `SELECT p.*, c.name as category_name
FROM products p FROM products p
JOIN categories c ON p.category_id = c.id JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id WHERE p.id = ?`,
WHERE p.id = ?`,
[productId] [productId]
); );
} }
static async getProductsByLocationAndCategory(locationId, categoryId, subcategoryId) { static async getProductsByLocationAndCategory(locationId, categoryId) {
return await db.allAsync( return await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url `SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ? WHERE location_id = ? AND category_id = ?
AND quantity_in_stock > 0 AND quantity_in_stock > 0
ORDER BY name`, ORDER BY name`,
[locationId, categoryId, subcategoryId] [locationId, categoryId]
); );
} }
static async getProductsByCategoryId(categoryId) {
return await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products
WHERE category_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[categoryId]
);
}
} }
export default ProductService export default ProductService