From 457dc74485d6eaa84b1030ffeab3aa9e649b61c6 Mon Sep 17 00:00:00 2001 From: vrubel Date: Tue, 30 Dec 2025 13:54:59 +0300 Subject: [PATCH] Add delete functionality for tasks and events in Telegram bot. Implemented commands for deleting items, including weekly tasks with scope options. Enhanced main menu and button handlers for better user interaction. Updated state management for deletion process. --- backend/telegram_bot.py | 318 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 290 insertions(+), 28 deletions(-) diff --git a/backend/telegram_bot.py b/backend/telegram_bot.py index 7f13e41..24fb805 100644 --- a/backend/telegram_bot.py +++ b/backend/telegram_bot.py @@ -4,7 +4,7 @@ from telegram.ext import ( ContextTypes, ConversationHandler, filters ) from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select +from sqlalchemy import select, and_ from datetime import datetime, timedelta import pytz import os @@ -24,6 +24,7 @@ BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") # Состояния для ConversationHandler SELECTING_DATE, SELECTING_HOUR, SELECTING_MINUTE, SELECTING_DURATION_HOUR, SELECTING_DURATION_MIN, ENTERING_TITLE = range(6) SELECTING_TASK_DATE, ENTERING_TASK_TITLE, SELECTING_REPEAT, SELECTING_COPY_DAYS = range(10, 14) +SELECTING_DELETE_DATE, SELECTING_DELETE_ITEM = range(20, 22) user_states = {} # Временное хранилище состояний пользователей @@ -100,23 +101,70 @@ def calculate_end_time(start_time: str, duration_min: int) -> str: return f"{end_hour:02d}:{end_minute:02d}" -async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /start""" - await register_user(str(update.effective_chat.id)) +async def show_main_menu(message): + """Показать главное меню с кнопками""" keyboard = [ [InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")], [InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")], - [InlineKeyboardButton("📅 Оставшиеся дни недели", callback_data="schedule_week")], + [InlineKeyboardButton("➕ Добавить занятие", callback_data="add_event")], + [InlineKeyboardButton("➕ Добавить задачу", callback_data="add_task")], + [InlineKeyboardButton("🗑 Удалить", callback_data="delete_start")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await message.reply_text( + "Выберите действие:", + reply_markup=reply_markup + ) + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /start""" + await register_user(str(update.effective_chat.id)) + await show_main_menu(update.message) + + +async def today_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /today""" + await register_user(str(update.effective_chat.id)) + today = datetime.now(TZ).date() + date_str = today.strftime("%Y-%m-%d") + text = await get_schedule_for_date(date_str) + await update.message.reply_text(text) + + +async def nextday_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /nextday""" + await register_user(str(update.effective_chat.id)) + today = datetime.now(TZ).date() + tomorrow = today + timedelta(days=1) + date_str = tomorrow.strftime("%Y-%m-%d") + text = await get_schedule_for_date(date_str) + await update.message.reply_text(text) + + +async def addtask_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /addtask""" + await register_user(str(update.effective_chat.id)) + keyboard = [ [InlineKeyboardButton("➕ Добавить занятие", callback_data="add_event")], [InlineKeyboardButton("➕ Добавить задачу", callback_data="add_task")], ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( - "Выберите действие:", + "Что вы хотите добавить?", reply_markup=reply_markup ) +async def deltask_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /deltask""" + await register_user(str(update.effective_chat.id)) + chat_id = str(update.effective_chat.id) + user_states[chat_id] = {"action": "delete"} + await show_date_selection_for_delete(update.message) + return SELECTING_DELETE_DATE + + async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик кнопок""" query = update.callback_query @@ -126,26 +174,23 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): today = datetime.now(TZ).date() if query.data == "schedule_today": + # Расписание на сегодня (аналог /today) - отправляем новое сообщение date_str = today.strftime("%Y-%m-%d") text = await get_schedule_for_date(date_str) - await query.edit_message_text(text) + await query.message.reply_text(text) elif query.data == "schedule_tomorrow": + # Расписание на завтра (аналог /nextday) - отправляем новое сообщение tomorrow = today + timedelta(days=1) date_str = tomorrow.strftime("%Y-%m-%d") text = await get_schedule_for_date(date_str) - await query.edit_message_text(text) + await query.message.reply_text(text) - elif query.data == "schedule_week": - # Оставшиеся дни недели - weekday = today.weekday() - text = "" - for i in range(weekday, 7): - date = today + timedelta(days=i - weekday) - date_str = date.strftime("%Y-%m-%d") - day_text = await get_schedule_for_date(date_str) - text += day_text + "\n" - await query.edit_message_text(text[:4000]) # Telegram limit + elif query.data == "delete_start": + # Начало процесса удаления (аналог /deltask) + user_states[chat_id] = {"action": "delete"} + await show_date_selection_for_delete(query) + return SELECTING_DELETE_DATE elif query.data == "add_event": # Начинаем процесс добавления event @@ -159,6 +204,15 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): await show_date_selection(query, "task") return SELECTING_TASK_DATE + elif query.data.startswith("del_date_"): + # Выбрана дата для удаления + date_str = query.data.replace("del_date_", "") + if chat_id not in user_states: + user_states[chat_id] = {} + user_states[chat_id]["delete_date"] = date_str + await show_items_for_deletion(query, date_str) + return SELECTING_DELETE_ITEM + elif query.data.startswith("date_"): # Выбрана дата date_str = query.data.replace("date_", "") @@ -197,16 +251,31 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): await query.edit_message_text("Введите название занятия:") return ENTERING_TITLE - # Возвращаемся в главное меню - keyboard = [ - [InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")], - [InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")], - [InlineKeyboardButton("📅 Оставшиеся дни недели", callback_data="schedule_week")], - [InlineKeyboardButton("➕ Добавить занятие", callback_data="add_event")], - [InlineKeyboardButton("➕ Добавить задачу", callback_data="add_task")], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Выберите действие:", reply_markup=reply_markup) + elif query.data.startswith("delete_"): + # Удаление элемента (формат: delete_ID_kind) + parts = query.data.replace("delete_", "").split("_") + if len(parts) >= 2: + item_id = int(parts[0]) + kind = parts[1] + await delete_item_by_id(query, item_id, kind) + if chat_id in user_states: + del user_states[chat_id] + return ConversationHandler.END + + elif query.data.startswith("delete_scope_"): + # Удаление weekly task с указанием области + parts = query.data.replace("delete_scope_", "").split("_") + if len(parts) >= 3: + item_id = int(parts[0]) + kind = parts[1] + scope = parts[2] # "one" или "series" + await delete_item_by_scope(query, item_id, kind, scope) + if chat_id in user_states: + del user_states[chat_id] + return ConversationHandler.END + + # Возвращаемся в главное меню после завершения операции + await show_main_menu(query.message) return ConversationHandler.END @@ -237,6 +306,37 @@ async def show_date_selection(query, kind="event"): await query.edit_message_text("Выберите дату:", reply_markup=reply_markup) +async def show_date_selection_for_delete(query_or_message): + """Показать выбор даты для удаления""" + today = datetime.now(TZ).date() + monday = today - timedelta(days=today.weekday()) + next_monday = monday + timedelta(days=7) + + keyboard = [] + weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] + + # Текущая неделя + for i in range(7): + date = monday + timedelta(days=i) + date_str = date.strftime("%Y-%m-%d") + label = f"{weekdays[i]} {date.day}" + keyboard.append([InlineKeyboardButton(label, callback_data=f"del_date_{date_str}")]) + + # Следующая неделя + for i in range(7): + date = next_monday + timedelta(days=i) + date_str = date.strftime("%Y-%m-%d") + label = f"{weekdays[i]} {date.day}" + keyboard.append([InlineKeyboardButton(label, callback_data=f"del_date_{date_str}")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + # Проверяем, это query или message + if hasattr(query_or_message, 'edit_message_text'): + await query_or_message.edit_message_text("Выберите дату для удаления:", reply_markup=reply_markup) + else: + await query_or_message.reply_text("Выберите дату для удаления:", reply_markup=reply_markup) + + async def show_hour_selection(query): """Показать выбор часа""" keyboard = [] @@ -395,6 +495,151 @@ async def handle_repeat_choice(update: Update, context: ContextTypes.DEFAULT_TYP return ConversationHandler.END +async def show_items_for_deletion(query, date_str: str): + """Показать список задач и занятий для удаления""" + async with AsyncSessionLocal() as db: + from backend.utils import materialize_weekly_tasks + + # Получаем tasks (разовые) + result = await db.execute( + select(Task).where( + and_(Task.date == date_str, Task.repeat_weekly == False) + ) + ) + tasks = result.scalars().all() + + # Получаем weekly tasks для этой даты + weekly_items = await materialize_weekly_tasks(db, date_str, date_str) + weekly_tasks = [item for item in weekly_items if item.kind == "task"] + + # Получаем events + result = await db.execute( + select(Event).where(Event.date == date_str) + ) + events = result.scalars().all() + events = sorted(events, key=lambda e: e.start_time) + + keyboard = [] + items_list = [] + + # Добавляем tasks + for task in tasks: + items_list.append(("task", task.id, task.title, None)) + keyboard.append([InlineKeyboardButton(f"📋 {task.title}", callback_data=f"delete_{task.id}_task")]) + + # Добавляем weekly tasks + for task_item in weekly_tasks: + # Для weekly tasks нужно найти оригинальную задачу + result = await db.execute( + select(Task).where(Task.id == task_item.id) + ) + task = result.scalar_one_or_none() + if task: + items_list.append(("task", task.id, task.title, None)) + keyboard.append([InlineKeyboardButton(f"📋 {task.title} (повторяется)", callback_data=f"delete_{task.id}_task")]) + + # Добавляем events + for event in events: + end_time = calculate_end_time(event.start_time, event.duration_min) + items_list.append(("event", event.id, event.title, event.start_time)) + keyboard.append([InlineKeyboardButton(f"⏰ {event.start_time}-{end_time} {event.title}", callback_data=f"delete_{event.id}_event")]) + + if not keyboard: + await query.edit_message_text(f"На {format_date_russian(date_str)} нет записей для удаления.") + return ConversationHandler.END + + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"Выберите запись для удаления на {format_date_russian(date_str)}:", + reply_markup=reply_markup + ) + + +async def delete_item_by_id(query, item_id: int, kind: str): + """Удалить элемент по ID""" + async with AsyncSessionLocal() as db: + if kind == "task": + result = await db.execute(select(Task).where(Task.id == item_id)) + task = result.scalar_one_or_none() + + if task: + if task.repeat_weekly: + # Для weekly task нужно спросить область применения + chat_id = str(query.from_user.id) + if chat_id not in user_states: + user_states[chat_id] = {} + user_states[chat_id]["delete_item_id"] = item_id + user_states[chat_id]["delete_kind"] = "task" + user_states[chat_id]["delete_date"] = user_states[chat_id].get("delete_date", "") + + keyboard = [ + [InlineKeyboardButton("Только на эту дату", callback_data=f"delete_scope_{item_id}_task_one")], + [InlineKeyboardButton("Для всего ряда", callback_data=f"delete_scope_{item_id}_task_series")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"Задача '{task.title}' повторяется каждую неделю. Что удалить?", + reply_markup=reply_markup + ) + return SELECTING_DELETE_ITEM + else: + await db.delete(task) + await db.commit() + await query.edit_message_text(f"✅ Задача '{task.title}' удалена.") + return ConversationHandler.END + + elif kind == "event": + result = await db.execute(select(Event).where(Event.id == item_id)) + event = result.scalar_one_or_none() + + if event: + title = event.title + await db.delete(event) + await db.commit() + await query.edit_message_text(f"✅ Занятие '{title}' удалено.") + return ConversationHandler.END + + await query.edit_message_text("❌ Запись не найдена.") + return ConversationHandler.END + + +async def delete_item_by_scope(query, item_id: int, kind: str, scope: str): + """Удалить элемент с указанием области (для weekly tasks)""" + async with AsyncSessionLocal() as db: + if kind == "task": + result = await db.execute(select(Task).where(Task.id == item_id)) + task = result.scalar_one_or_none() + + if task and task.repeat_weekly: + chat_id = str(query.from_user.id) + state = user_states.get(chat_id, {}) + date_str = state.get("delete_date", "") + + if scope == "one": + # Создаем исключение на удаление для конкретной даты + from backend.database import WeeklyTaskException + weekday = get_weekday_from_date(date_str) + exception = WeeklyTaskException( + weekday=weekday, + date=date_str, + action="delete" + ) + db.add(exception) + await db.commit() + await query.edit_message_text(f"✅ Задача '{task.title}' удалена только на {format_date_russian(date_str)}.") + elif scope == "series": + # Удаляем весь ряд weekly tasks + await db.execute( + Task.__table__.delete().where(Task.weekday == task.weekday) + ) + await db.commit() + await query.edit_message_text(f"✅ Задача '{task.title}' удалена для всего ряда.") + return ConversationHandler.END + + await query.edit_message_text("❌ Ошибка при удалении.") + return ConversationHandler.END + + async def send_reminders(): """Отправка напоминаний о событиях""" while True: @@ -477,7 +722,24 @@ async def start_bot(): name="task_conv", ) + # Команды application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("today", today_command)) + application.add_handler(CommandHandler("nextday", nextday_command)) + application.add_handler(CommandHandler("addtask", addtask_command)) + + # Conversation handler для удаления + delete_conv = ConversationHandler( + entry_points=[CommandHandler("deltask", deltask_command)], + states={ + SELECTING_DELETE_DATE: [CallbackQueryHandler(button_handler, pattern="^del_date_")], + SELECTING_DELETE_ITEM: [CallbackQueryHandler(button_handler, pattern="^(delete_|delete_scope_)")], + }, + fallbacks=[], + name="delete_conv", + ) + + application.add_handler(delete_conv) application.add_handler(event_conv) application.add_handler(task_conv) application.add_handler(CallbackQueryHandler(button_handler))