feat: admin product deletion
This commit is contained in:
@@ -1,21 +1,40 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from dto.catalogue import CategoryActionContext
|
||||
from dto.catalogue import CategoryActionContext, ProductActionContext
|
||||
from misc.filters import IsVerified
|
||||
from misc.kb.client import created_category, get_back_to_catalogue
|
||||
from misc.kb.common import return_menu
|
||||
from misc.mapper import parse_cat_id
|
||||
from misc.states import AdminControlStorage
|
||||
from repositories.categories import CategoriesRepository
|
||||
from repositories.products import ProductRepository
|
||||
from services.catalogue import CatalogueService
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(IsVerified(), F.data.startswith("new:"))
|
||||
def skip_product_description_kb(parent_id: str) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Пропустить", callback_data="new_product:skip_description")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def skip_product_photo_kb(parent_id: str) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Пропустить", callback_data="new_product:skip_photo")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(IsVerified(), F.data.regexp(r"^new:(category|product):.+$"))
|
||||
async def creation_init(
|
||||
cb: CallbackQuery,
|
||||
state: FSMContext,
|
||||
@@ -41,7 +60,12 @@ async def creation_init(
|
||||
await state.set_data({"ctx": ctx})
|
||||
|
||||
if mode == "product":
|
||||
...
|
||||
await cb.message.edit_text(
|
||||
f"{path} <i>-> ...</i>\n\n<b>📦 Введите название товара.</b>",
|
||||
reply_markup=get_back_to_catalogue(cat_id),
|
||||
)
|
||||
await state.set_state(AdminControlStorage.new_product_name)
|
||||
await state.set_data({"ctx": ProductActionContext(cb.message, cat_id)})
|
||||
|
||||
|
||||
@router.message(AdminControlStorage.new_category)
|
||||
@@ -70,3 +94,131 @@ async def create_new_category(
|
||||
category.id, f"🛒 {category.name}", category.parent_id or "root"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.message(IsVerified(), AdminControlStorage.new_product_name)
|
||||
async def create_product_name(msg: Message, state: FSMContext):
|
||||
if not msg.text or not msg.text.strip():
|
||||
await msg.reply("❌ Название не может быть пустым.")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
ctx: ProductActionContext | None = data.get("ctx")
|
||||
if not ctx:
|
||||
await state.clear()
|
||||
await msg.reply("❌ Ошибка при создании товара.", reply_markup=return_menu)
|
||||
return
|
||||
|
||||
await msg.delete()
|
||||
await ctx.msg.edit_text(
|
||||
"<b>📜 Введите описание товара.</b>",
|
||||
reply_markup=skip_product_description_kb(str(ctx.parent_id)),
|
||||
)
|
||||
await state.update_data(name=msg.text.strip())
|
||||
await state.set_state(AdminControlStorage.new_product_description)
|
||||
|
||||
|
||||
@router.callback_query(IsVerified(), F.data == "new_product:skip_description")
|
||||
async def skip_product_description(cb: CallbackQuery, state: FSMContext):
|
||||
data = await state.get_data()
|
||||
ctx: ProductActionContext | None = data.get("ctx")
|
||||
if not ctx:
|
||||
await state.clear()
|
||||
await cb.answer("Ошибка при создании товара.", show_alert=True)
|
||||
return
|
||||
|
||||
await cb.message.edit_text(
|
||||
"<b>💸 Введите цену товара целым числом.</b>",
|
||||
reply_markup=get_back_to_catalogue(ctx.parent_id),
|
||||
)
|
||||
await state.update_data(description=None)
|
||||
await state.set_state(AdminControlStorage.new_product_price)
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.message(IsVerified(), AdminControlStorage.new_product_description)
|
||||
async def create_product_description(msg: Message, state: FSMContext):
|
||||
if not msg.text or not msg.text.strip():
|
||||
await msg.reply("❌ Описание не может быть пустым.")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
ctx: ProductActionContext | None = data.get("ctx")
|
||||
if not ctx:
|
||||
await state.clear()
|
||||
await msg.reply("❌ Ошибка при создании товара.", reply_markup=return_menu)
|
||||
return
|
||||
|
||||
await msg.delete()
|
||||
await ctx.msg.edit_text(
|
||||
"<b>💸 Введите цену товара целым числом.</b>",
|
||||
reply_markup=get_back_to_catalogue(ctx.parent_id),
|
||||
)
|
||||
await state.update_data(description=msg.text.strip())
|
||||
await state.set_state(AdminControlStorage.new_product_price)
|
||||
|
||||
|
||||
@router.message(IsVerified(), AdminControlStorage.new_product_price)
|
||||
async def create_product_price(
|
||||
msg: Message,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
products_repo: ProductRepository,
|
||||
):
|
||||
if not msg.text or not msg.text.isdigit() or int(msg.text) <= 0:
|
||||
await msg.reply("❌ Цена должна быть положительным целым числом.")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
ctx: ProductActionContext | None = data.get("ctx")
|
||||
if not ctx:
|
||||
await state.clear()
|
||||
await msg.reply("❌ Ошибка при создании товара.", reply_markup=return_menu)
|
||||
return
|
||||
|
||||
await msg.delete()
|
||||
product = await products_repo.add_product(
|
||||
session,
|
||||
category_id=parse_cat_id(ctx.parent_id),
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
price=int(msg.text),
|
||||
)
|
||||
await ctx.msg.edit_text(
|
||||
"✅ Основные данные сохранены.\n\n<b>📷 Отправьте фотографию товара или пропустите этот шаг.</b>",
|
||||
reply_markup=skip_product_photo_kb(str(ctx.parent_id)),
|
||||
)
|
||||
await state.update_data(product_id=product.id)
|
||||
await state.set_state(AdminControlStorage.new_product_photo)
|
||||
|
||||
|
||||
@router.callback_query(IsVerified(), F.data == "new_product:skip_photo")
|
||||
async def skip_product_photo(cb: CallbackQuery, state: FSMContext):
|
||||
data = await state.get_data()
|
||||
ctx: ProductActionContext | None = data.get("ctx")
|
||||
await state.clear()
|
||||
parent_id = ctx.parent_id if ctx else "root"
|
||||
await cb.message.edit_text("✅ Товар добавлен.", reply_markup=get_back_to_catalogue(parent_id))
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.message(IsVerified(), AdminControlStorage.new_product_photo)
|
||||
async def create_product_photo(
|
||||
msg: Message,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
products_repo: ProductRepository,
|
||||
):
|
||||
data = await state.get_data()
|
||||
if not msg.photo or not data.get("product_id"):
|
||||
await msg.delete()
|
||||
await msg.reply("❌ Отправьте именно фотографию или нажмите «Пропустить».")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
await msg.delete()
|
||||
await products_repo.add_product_file_id(
|
||||
session, int(data["product_id"]), file_id=msg.photo[-1].file_id
|
||||
)
|
||||
ctx: ProductActionContext = data["ctx"]
|
||||
await ctx.msg.edit_text("✅ Товар добавлен.", reply_markup=get_back_to_catalogue(ctx.parent_id))
|
||||
|
||||
@@ -13,8 +13,7 @@ from misc.kb.admins import back_to_product_kb
|
||||
from misc.kb.client import get_back_to_catalogue
|
||||
from misc.mapper import parse_cat_id
|
||||
from misc.states import AdminControlStorage
|
||||
from misc.texts import get_product_description
|
||||
from misc.texts import product_editing_mapping
|
||||
from misc.texts import get_product_description, product_editing_mapping
|
||||
from repositories.categories import CategoriesRepository
|
||||
from repositories.products import ProductRepository
|
||||
|
||||
@@ -210,6 +209,42 @@ async def delete_product_photo(
|
||||
await cb.message.edit_reply_markup(reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(IsVerified(), F.data.regexp(r"^edit_product:\d+:delete$"))
|
||||
async def request_product_deletion(cb: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
product_id = int(cb.data.split(":")[1])
|
||||
await cb.message.edit_reply_markup(reply_markup=admins.confirm_product_deletion(product_id))
|
||||
await cb.answer("Подтвердите удаление товара.")
|
||||
|
||||
|
||||
@router.callback_query(IsVerified(), F.data.regexp(r"^delete_product:confirm:\d+$"))
|
||||
async def delete_product(
|
||||
cb: CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
products_repo: ProductRepository,
|
||||
):
|
||||
await state.clear()
|
||||
product_id = int(cb.data.split(":")[2])
|
||||
product = await products_repo.get_product_by_id(session, product_id)
|
||||
if not product:
|
||||
await cb.answer("Товар уже удалён.", show_alert=True)
|
||||
return
|
||||
|
||||
category_id = product.category_id
|
||||
try:
|
||||
await products_repo.delete_product(session, product_id)
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to delete product %s", product_id)
|
||||
await cb.answer("Не удалось удалить товар: он используется в заказах.", show_alert=True)
|
||||
return
|
||||
|
||||
await cb.message.delete()
|
||||
await cb.message.answer("✅ Товар удалён.", reply_markup=get_back_to_catalogue(category_id))
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("edit:"))
|
||||
async def edit_entry(cb: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
|
||||
Reference in New Issue
Block a user