feat: admin product deletion
This commit is contained in:
@@ -1,21 +1,40 @@
|
|||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.fsm.context import FSMContext
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from dto.catalogue import CategoryActionContext
|
from dto.catalogue import CategoryActionContext, ProductActionContext
|
||||||
from misc.filters import IsVerified
|
from misc.filters import IsVerified
|
||||||
from misc.kb.client import created_category, get_back_to_catalogue
|
from misc.kb.client import created_category, get_back_to_catalogue
|
||||||
from misc.kb.common import return_menu
|
from misc.kb.common import return_menu
|
||||||
from misc.mapper import parse_cat_id
|
from misc.mapper import parse_cat_id
|
||||||
from misc.states import AdminControlStorage
|
from misc.states import AdminControlStorage
|
||||||
from repositories.categories import CategoriesRepository
|
from repositories.categories import CategoriesRepository
|
||||||
|
from repositories.products import ProductRepository
|
||||||
from services.catalogue import CatalogueService
|
from services.catalogue import CatalogueService
|
||||||
|
|
||||||
router = Router()
|
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(
|
async def creation_init(
|
||||||
cb: CallbackQuery,
|
cb: CallbackQuery,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
@@ -41,7 +60,12 @@ async def creation_init(
|
|||||||
await state.set_data({"ctx": ctx})
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
if mode == "product":
|
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)
|
@router.message(AdminControlStorage.new_category)
|
||||||
@@ -70,3 +94,131 @@ async def create_new_category(
|
|||||||
category.id, f"🛒 {category.name}", category.parent_id or "root"
|
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.kb.client import get_back_to_catalogue
|
||||||
from misc.mapper import parse_cat_id
|
from misc.mapper import parse_cat_id
|
||||||
from misc.states import AdminControlStorage
|
from misc.states import AdminControlStorage
|
||||||
from misc.texts import get_product_description
|
from misc.texts import get_product_description, product_editing_mapping
|
||||||
from misc.texts import product_editing_mapping
|
|
||||||
from repositories.categories import CategoriesRepository
|
from repositories.categories import CategoriesRepository
|
||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
|
|
||||||
@@ -210,6 +209,42 @@ async def delete_product_photo(
|
|||||||
await cb.message.edit_reply_markup(reply_markup=reply_markup)
|
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:"))
|
@router.callback_query(F.data.startswith("edit:"))
|
||||||
async def edit_entry(cb: CallbackQuery, state: FSMContext):
|
async def edit_entry(cb: CallbackQuery, state: FSMContext):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|||||||
@@ -79,3 +79,16 @@ def edit_product(product: Product):
|
|||||||
[InlineKeyboardButton(text="⬅️", callback_data=f"cat:{product.category_id}")],
|
[InlineKeyboardButton(text="⬅️", callback_data=f"cat:{product.category_id}")],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_product_deletion(product_id: int) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardBuilder(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="✅ Да, удалить", callback_data=f"delete_product:confirm:{product_id}"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(text="❌ Отмена", callback_data=f"product:{product_id}"),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
).as_markup()
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ class AdminControlStorage(StatesGroup):
|
|||||||
edit_product_photo = State()
|
edit_product_photo = State()
|
||||||
edit_category = State()
|
edit_category = State()
|
||||||
new_category = State()
|
new_category = State()
|
||||||
|
new_product_name = State()
|
||||||
|
new_product_description = State()
|
||||||
|
new_product_price = State()
|
||||||
|
new_product_photo = State()
|
||||||
|
|||||||
@@ -7,6 +7,35 @@ from db.models import Product
|
|||||||
|
|
||||||
|
|
||||||
class ProductRepository:
|
class ProductRepository:
|
||||||
|
async def delete_product(self, session: AsyncSession, product_id: int) -> Product | None:
|
||||||
|
product = await self.get_product_by_id(session, product_id)
|
||||||
|
if not product:
|
||||||
|
return None
|
||||||
|
|
||||||
|
await session.delete(product)
|
||||||
|
await session.commit()
|
||||||
|
return product
|
||||||
|
|
||||||
|
async def add_product(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
category_id: int | None,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
price: int,
|
||||||
|
) -> Product:
|
||||||
|
product = Product(
|
||||||
|
category_id=category_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
price=price,
|
||||||
|
)
|
||||||
|
session.add(product)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(product)
|
||||||
|
return product
|
||||||
|
|
||||||
async def get_product_by_id(
|
async def get_product_by_id(
|
||||||
self, session: AsyncSession, product_id: int, *, include_hidden: bool = True
|
self, session: AsyncSession, product_id: int, *, include_hidden: bool = True
|
||||||
) -> Product | None:
|
) -> Product | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user