diff --git a/sql/schema.sql b/sql/schema.sql index a6c5072..965b74e 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -14,6 +14,7 @@ CREATE TABLE users ( sleep_time TIME DEFAULT NULL ); +-- TABLE routines -- CREATE TABLE routines ( id SERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -21,6 +22,15 @@ CREATE TABLE routines ( routine_name VARCHAR(255) NOT NULL ); +-- TABLE Focuses -- +CREATE TABLE focuses ( + id SERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + duration VARCHAR(255) NOT NULL, + title VARCHAR(225), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + -- TABLE tasks CREATE TABLE tasks ( id SERIAL PRIMARY KEY, diff --git a/telegram_bot_project/bot/buttons.py b/telegram_bot_project/bot/buttons.py index b028438..b1958ea 100644 --- a/telegram_bot_project/bot/buttons.py +++ b/telegram_bot_project/bot/buttons.py @@ -159,9 +159,13 @@ def evening_routine_keyboard() -> ReplyKeyboardMarkup: def focus_menu_keyboard(started: Optional[bool] = False) -> ReplyKeyboardMarkup: menu_btn = KeyboardButton(text=MENU_BUTTON) focus_btn = KeyboardButton(text=FOCUS_ZONE_END if started else FOCUS_ZONE_START) + focus_list = KeyboardButton(text=ALL_FOCUSES_BTN) + delete_focus = KeyboardButton(text=DELETE_FOCUS) keyboard = [ [focus_btn], + [focus_list], + [delete_focus], [menu_btn] ] @@ -173,9 +177,21 @@ def focus_menu_keyboard(started: Optional[bool] = False) -> ReplyKeyboardMarkup: def focus_save_question_keyboard() -> InlineKeyboardMarkup: inline_markup = InlineKeyboardMarkup(inline_keyboard=[], row_width=2) - save_focus_btn: InlineKeyboardButton = InlineKeyboardButton(text=FOCUS_INLINE_YES, callback_data="save_focus") - not_save_focus_btn: InlineKeyboardButton = InlineKeyboardButton(text=FOCUS_INLINE_NO, callback_data="not_save_focus") + save_focus_btn = InlineKeyboardButton(text=FOCUS_INLINE_YES, callback_data="save_focus") + not_save_focus_btn = InlineKeyboardButton(text=FOCUS_INLINE_NO, callback_data="not_save_focus") + + inline_markup.inline_keyboard.append([save_focus_btn, not_save_focus_btn]) - inline_markup.add(save_focus_btn) - inline_markup.add(not_save_focus_btn) return inline_markup + + +def focus_title_ask_keyboard() -> InlineKeyboardMarkup: + inline_markup = InlineKeyboardMarkup(inline_keyboard=[], row_width=2) + + add_title = InlineKeyboardButton(text=FOCUS_INLINE_YES, callback_data="add_title") + not_add_title = InlineKeyboardButton(text=FOCUS_INLINE_NO, callback_data="not_add_title") + + inline_markup.inline_keyboard.append([add_title, not_add_title]) + + return inline_markup + diff --git a/telegram_bot_project/bot/callbacks.py b/telegram_bot_project/bot/callbacks.py index bd3a530..1d008ef 100644 --- a/telegram_bot_project/bot/callbacks.py +++ b/telegram_bot_project/bot/callbacks.py @@ -9,6 +9,7 @@ from service.task import TaskService from service.user import UserService from states import DialogStates +from service.focus import FocusService async def start_callback_language(callback_query: types.CallbackQuery) -> None: await callback_query.answer() @@ -129,4 +130,53 @@ async def callback_routines(callback_query: types.CallbackQuery) -> None: case "evening_view": await callback_query.message.answer(MESSAGES[language]['EVENING_ROUTINE'], reply_markup=evening_routine_keyboard()) case _: - await callback_query.message.answer(MESSAGES[language]["ROUTINES_INVALID"]) \ No newline at end of file + await callback_query.message.answer(MESSAGES[language]["ROUTINES_INVALID"]) + +async def callback_focus(callback_query: types.CallbackQuery, state: FSMContext) -> None: + await callback_query.answer() + + user_id: int = callback_query.from_user.id + user_find: Optional[dict] = await UserService.get_user_by_id(user_id) + language: str = await UserService.get_user_language(user_id) + if not language: + language = 'ENGLISH' + if not user_find: + await callback_query.message.answer(MESSAGES['ENGLISH']["AUTHORIZATION_PROBLEM"]) + return + + match callback_query.data: + case "save_focus": + await callback_query.message.answer(MESSAGES[language]["TITLE_FOCUS_ZONE_MSG"], reply_markup=focus_title_ask_keyboard()) + case "not_save_focus": + await callback_query.message.answer(MESSAGES[language]["NOT_SAVED_FOCUS_MSG"], reply_markup=focus_menu_keyboard()) + case _: + await callback_query.message.answer(MESSAGES[language]["FOCUS_INVALID"], reply_markup=focus_menu_keyboard()) + +async def callback_focus_title(callback_query: types.CallbackQuery, state: FSMContext) -> None: + await callback_query.answer() + + user_id: int = callback_query.from_user.id + user_find: Optional[dict] = await UserService.get_user_by_id(user_id) + language: str = await UserService.get_user_language(user_id) + if not language: + language = 'ENGLISH' + if not user_find: + await callback_query.message.answer(MESSAGES['ENGLISH']["AUTHORIZATION_PROBLEM"]) + return + + match callback_query.data: + case "add_title": + await state.set_state(DialogStates.provide_title_focusing) + await callback_query.message.answer(MESSAGES[language]["FOCUS_TITLE_ASK"]) + case "not_add_title": + print(f"[INFO] - User {user_id} canceled adding title.") + + data = await state.get_data() + time_d = data.get("duration") + await FocusService.create_focus(user_id, duration=time_d) + + print(f"[INFO] - User {user_id} saved focus: {time_d}") + await callback_query.message.answer(MESSAGES[language]["SAVED_FOCUS_MSG"], reply_markup=focus_menu_keyboard()) + await state.clear() + case _: + await callback_query.message.answer(MESSAGES[language]["FOCUS_INVALID"], reply_markup=focus_menu_keyboard()) \ No newline at end of file diff --git a/telegram_bot_project/bot/commands.py b/telegram_bot_project/bot/commands.py index ac6857e..d62037d 100644 --- a/telegram_bot_project/bot/commands.py +++ b/telegram_bot_project/bot/commands.py @@ -1,7 +1,7 @@ # bot/commands.py -from typing import List from aiogram.fsm.context import FSMContext +from service.focus import FocusService from bot.utills import format_date, calculate_awake_hours from service.idea import IdeaService from service.task import TaskService @@ -446,4 +446,54 @@ async def show_focus_menu(message: types.Message) -> None: await message.answer(MESSAGES['ENGLISH']['AUTHORIZATION_PROBLEM']) else: print(f"[INFO] - User with id: {user_id} - opened /focus.") - await message.answer(MESSAGES[language]['WELCOME_TO_FOCUS'], reply_markup=focus_menu_keyboard()) \ No newline at end of file + await message.answer(MESSAGES[language]['WELCOME_TO_FOCUS'], reply_markup=focus_menu_keyboard()) + +# Show All Focus Sessions Handler +async def show_all_focuses(message: types.Message) -> None: + user_id: int = message.from_user.id + user_find: Any = await UserService.get_user_by_id(user_id) + language: str = await UserService.get_user_language(user_id) or "ENGLISH" + + if not user_find: + await message.answer(MESSAGES[language]['AUTHORIZATION_PROBLEM']) + return + + print(f"[INFO] - User with id: {user_id} - opened /show_all_focuses.") + focuses = await FocusService.get_focuses_by_user(user_id) + + if not focuses: + await message.answer(MESSAGES[language]['NO_FOCUS_SESSIONS']) + return + + dividers: str = "\n" + ("-" * int(len(MESSAGES[language]['FOCUS_LIST_TITLE']) * 1.65)) + formatted_focuses = "\n".join( + f"# {idx}. {focus['title']} ({focus['duration']}) – {focus['created_at'].strftime('%Y-%m-%d %H:%M')}" + for idx, focus in enumerate(focuses, start=1) + ) + + formatted_response = ( + MESSAGES[language]['FOCUS_LIST_TITLE'] + + dividers + + "\n" + + formatted_focuses + ) + + await message.answer(formatted_response, reply_markup=focus_menu_keyboard()) + +# Delete Focus Session Handler +async def delete_focus_session(message: types.Message, state: FSMContext) -> None: + user_id: int = message.from_user.id + user_find: Any = await UserService.get_user_by_id(user_id) + language: str = await UserService.get_user_language(user_id) or "ENGLISH" + + if not user_find: + await message.answer(MESSAGES['ENGLISH']['AUTHORIZATION_PROBLEM']) + else: + focuses = await FocusService.get_focuses_by_user(user_id) + if not focuses: + await message.answer(MESSAGES[language]['NO_FOCUS_SESSIONS']) + return + + await state.update_data(focuses=focuses) + await state.set_state(DialogStates.delete_focus) + await message.answer(MESSAGES[language]['DELETE_FOCUS_SESSION_MSG']) diff --git a/telegram_bot_project/bot/fallbacks.py b/telegram_bot_project/bot/fallbacks.py index 37b60ab..c0a899e 100644 --- a/telegram_bot_project/bot/fallbacks.py +++ b/telegram_bot_project/bot/fallbacks.py @@ -46,4 +46,8 @@ async def fallback(message: types.Message, state: FSMContext): routine_type = data.get("routine_type", "morning") await process_update_morning_routine(message, state, type=routine_type) elif current_state == DialogStates.feedback_message: - await process_feedback_message(message, state) \ No newline at end of file + await process_feedback_message(message, state) + elif current_state == DialogStates.provide_title_focusing: + await process_save_focus_session_title(message, state) + elif current_state == DialogStates.delete_focus: + await process_delete_focus_session(message, state) \ No newline at end of file diff --git a/telegram_bot_project/bot/handlers.py b/telegram_bot_project/bot/handlers.py index 073c138..d6ef669 100644 --- a/telegram_bot_project/bot/handlers.py +++ b/telegram_bot_project/bot/handlers.py @@ -14,6 +14,7 @@ from bot.utills import check_valid_time, validate_text from service.myday import MyDayService from bot.scheduler import update_user_job +from service.focus import FocusService async def process_idea_save(message: Message, state: FSMContext) -> None: user_id = message.from_user.id @@ -500,4 +501,60 @@ async def process_feedback_message(message: Message, state: FSMContext): print(f"[INFO] - Feedback message from user with id: {user_id} is: {feedback_message}") await SmtpService.send_feedback_message(feedback_message, user_id, user_name) await message.answer(MESSAGES[language]['SMTP_MESSAGE_SENT'], reply_markup=settings_menu_keyboard()) - await state.clear() \ No newline at end of file + await state.clear() + +async def process_save_focus_session_title(message: Message, state: FSMContext): + user_id = message.from_user.id + user_find = await UserService.get_user_by_id(user_id) + language = await UserService.get_user_language(user_id) or "ENGLISH" + + if not user_find: + await message.answer(MESSAGES["ENGLISH"]['AUTHORIZATION_PROBLEM']) + return + + focus_title: str = message.text.strip() + if not validate_text(focus_title): + await message.answer(MESSAGES[language]['FOCUS_INVALID']) + return + + data = await state.get_data() + time_d = data.get("duration") + + try: + await FocusService.create_focus(user_id, time_d, focus_title) + + print(f"[INFO] - User with id: {user_id} created focus session with title: {focus_title}") + await message.answer(MESSAGES[language]['SAVED_FOCUS_MSG'].format(focus_title), reply_markup=focus_menu_keyboard()) + await state.clear() + except Exception as e: + print(f"[ERROR] - {e}") + await message.answer(MESSAGES[language]['FOCUS_INVALID']) + +async def process_delete_focus_session(message: Message, state: FSMContext): + user_id = message.from_user.id + user_find = await UserService.get_user_by_id(user_id) + language = await UserService.get_user_language(user_id) or "ENGLISH" + + if not user_find: + await message.answer(MESSAGES["ENGLISH"]['AUTHORIZATION_PROBLEM']) + return + + number = message.text.strip() + if not number.isdigit(): + await message.answer(MESSAGES[language]['FOCUS_INVALID']) + return + + data = await state.get_data() + focus_sessions = data.get("focuses") + + try: + focus_to_delete = focus_sessions[int(number) - 1] + real_id = focus_to_delete["id"] + + print(f"[INFO] - User with id: {user_id} deleted focus session with id: {real_id}") + await FocusService.delete_focus(real_id) + await message.answer(MESSAGES[language]['FOCUS_DELETED'].format(number, focus_to_delete['title']), reply_markup=focus_menu_keyboard()) + await state.clear() + except Exception as e: + print(f"[ERROR] - {e}") + await message.answer(MESSAGES[language]['FOCUS_INVALID']) \ No newline at end of file diff --git a/telegram_bot_project/main.py b/telegram_bot_project/main.py index 979494b..99baf62 100644 --- a/telegram_bot_project/main.py +++ b/telegram_bot_project/main.py @@ -192,7 +192,7 @@ async def focus_start(message: Message): @dp.message(lambda m: m.text == FOCUS_ZONE_END) -async def focus_end(message: Message): +async def focus_end(message: Message, state: FSMContext): user_id = message.from_user.id user_find = await UserService.get_user_by_id(user_id) language = await UserService.get_user_language(user_id) @@ -207,7 +207,10 @@ async def focus_end(message: Message): start_time = focus_times.get(user_id) if not start_time: - await message.answer("❗ Не знайдено початку фокус-сесії.") + await message.answer( + MESSAGES[language]['NOT_FOUND_FOCUS_SESSION'], + reply_markup=focus_menu_keyboard(FOCUS_STATUS) + ) return end_time = datetime.now() @@ -219,12 +222,21 @@ async def focus_end(message: Message): focus_times.pop(user_id, None) + await state.update_data(duration=f"{minutes}m:{seconds}s") await message.answer( - MESSAGES[language]['STOP_FOCUS_MSG'].format(minutes, seconds), - reply_markup=focus_menu_keyboard(FOCUS_STATUS) + MESSAGES[language]['STOP_FOCUS_MSG'].format(minutes, seconds) + + '\n' + MESSAGES[language]['SAVE_FOCUS_ZONE'], + reply_markup=focus_save_question_keyboard() ) +@dp.message(Command("focuses")) +@dp.message(lambda m: m.text == ALL_FOCUSES_BTN) +async def show_saved_focus(message: Message): + await show_all_focuses(message) +@dp.message(lambda m: m.text == DELETE_FOCUS) +async def delete_focus(message: Message, state: FSMContext): + await delete_focus_session(message, state) @dp.callback_query(F.data.in_({"morning_view", "evening_view"})) async def callback_routine(callback_query: CallbackQuery): @@ -242,6 +254,14 @@ async def callback_idea(callback_query: CallbackQuery, state: FSMContext): async def callback_idea(callback_query: CallbackQuery, state: FSMContext): await callback_task_deadline(callback_query, state) +@dp.callback_query(F.data.in_({"save_focus", "not_save_focus"})) +async def callback_focus_save(callback_query: CallbackQuery, state: FSMContext): + await callback_focus(callback_query, state) + +@dp.callback_query(F.data.in_({"add_title", "not_add_title"})) +async def callback_focus_title_save(callback_query: CallbackQuery, state: FSMContext): + await callback_focus_title(callback_query, state) + @dp.message() async def process_fallback(message: Message, state: FSMContext): await fallback(message, state) diff --git a/telegram_bot_project/messages.py b/telegram_bot_project/messages.py index 098d504..a2a2a41 100644 --- a/telegram_bot_project/messages.py +++ b/telegram_bot_project/messages.py @@ -106,13 +106,22 @@ "STOP_FOCUS_MSG": "Сесію фокусу зупинено.\nТривалість: - {}хв {}с. ⏳", "SAVE_FOCUS_ZONE": "Зберегти сесію фокусу? 📝", "SAVED_FOCUS_MSG": "Сесію фокусу збережено. ✅", + "NOT_SAVED_FOCUS_MSG": "Сесію фокусу не збережено. ❌", "TITLE_FOCUS_ZONE_MSG": "Бажаєте дати назву цій сесії? 📝", + "NOT_FOUND_FOCUS_SESSION": "❗ Не знайдено початку фокус-сесії.", + "FOCUS_INVALID": "❗ Неправильний параметр фокусування", + "FOCUS_TITLE_ASK": "Будь ласка, введіть назву фокус-сесії.", + "FOCUS_EXISTS": "❗ Фокус-сесія вже активна.", + "FOCUS_LIST_TITLE": "🧠 Список ваших фокус-сесій", + "NO_FOCUS_SESSIONS": "😕 У вас ще немає жодної фокус-сесії.", "LANGUAGE_ASK": ( "🌐 Виберіть мову для роботи. \n" "Оберіть опцію нижче. 📚" ), "LANGUAGE_OK": "✅ Мову змінено. Готові продовжити? 🚀", - "LANGUAGE_INVALID": "❌ Некоректний вибір мови. Спробуйте ще раз. 🔢" + "LANGUAGE_INVALID": "❌ Некоректний вибір мови. Спробуйте ще раз. 🔢", + "DELETE_FOCUS_SESSION_MSG": "Вкажіть номер сесії, яку ви хочете видалити.", + "FOCUS_DELETED": "✅ Фокус-сесію №{} з назвою \"{}\" успішно видалено.", }, "ENGLISH": { "START_MSG": ( @@ -216,13 +225,22 @@ "STOP_FOCUS_MSG": "Focus session stopped.\nDuration - {}m {}s. ⏳", "SAVE_FOCUS_ZONE": "Save the focus session? 📝", "SAVED_FOCUS_MSG": "Focus session saved. ✅", + "NOT_SAVED_FOCUS_MSG": "Focus session not saved. ❌", "TITLE_FOCUS_ZONE_MSG": "Would you like to name this session? 📝", + "NOT_FOUND_FOCUS_SESSION": "❗ Focus session start not found.", + "FOCUS_INVALID": "Invalid option for focus", + "FOCUS_TITLE_ASK": "Please provide the title for focus session.", + "FOCUS_EXISTS": "❗ A focus session is already active.", + "FOCUS_LIST_TITLE": "🧠 Your Focus Sessions", + "NO_FOCUS_SESSIONS": "😕 No focus sessions found.", + "DELETE_FOCUS_SESSION_MSG": "Provide a number of session which you want to delete.", "LANGUAGE_ASK": ( "🌐 Choose your language. \n" "Select an option below. 📚" ), "LANGUAGE_OK": "✅ Language updated. Ready to proceed? 🚀", - "LANGUAGE_INVALID": "❌ Invalid language choice. Try again. 🔢" + "LANGUAGE_INVALID": "❌ Invalid language choice. Try again. 🔢", + "FOCUS_DELETED": "✅ Focus session #{} with the title \"{}\" has been successfully deleted.", } } @@ -270,6 +288,8 @@ FOCUS_ZONE_END = "🔴 Stop" FOCUS_INLINE_YES = "Yes" FOCUS_INLINE_NO = "No" +ALL_FOCUSES_BTN = "All Focuses" +DELETE_FOCUS = "Delete" USER_FEEDBACK_MAIL_TEXT = """ 📬 New feedback received! diff --git a/telegram_bot_project/service/focus.py b/telegram_bot_project/service/focus.py new file mode 100644 index 0000000..431465d --- /dev/null +++ b/telegram_bot_project/service/focus.py @@ -0,0 +1,53 @@ +from sqlalchemy import text +from config import get_session +from typing import Optional + +class FocusService: + @staticmethod + async def create_focus(user_id: int, duration: int, title: Optional[str] = None) -> int: + async with get_session() as session: + result = await session.execute( + text(""" + INSERT INTO focuses (user_id, duration, title) + VALUES (:user_id, :duration, :title) + RETURNING id + """), + {"user_id": user_id, "duration": duration, "title": title} + ) + await session.commit() + return result.scalar_one() + + @staticmethod + async def get_focus_by_id(focus_id: int) -> Optional[dict]: + async with get_session() as session: + result = await session.execute( + text("SELECT * FROM focuses WHERE id = :id"), + {"id": focus_id} + ) + row = result.first() + return dict(row._mapping) if row else None + + @staticmethod + async def get_focuses_by_user(user_id: int) -> list[dict]: + async with get_session() as session: + result = await session.execute( + text(""" + SELECT id, user_id, duration, title, created_at + FROM focuses + WHERE user_id = :user_id + ORDER BY created_at DESC + """), + {"user_id": user_id} + ) + rows = result.fetchall() + return [dict(row._mapping) for row in rows] + + @staticmethod + async def delete_focus(focus_id: int) -> bool: + async with get_session() as session: + result = await session.execute( + text("DELETE FROM focuses WHERE id = :id"), + {"id": focus_id} + ) + await session.commit() + return result.rowcount == 1 diff --git a/telegram_bot_project/states.py b/telegram_bot_project/states.py index 3c59604..18152cc 100644 --- a/telegram_bot_project/states.py +++ b/telegram_bot_project/states.py @@ -18,4 +18,6 @@ class DialogStates(StatesGroup): delete_morning_routine = State() update_morning_routine = State() update_morning_routine_id = State() - feedback_message = State() \ No newline at end of file + feedback_message = State() + provide_title_focusing = State() + delete_focus = State() \ No newline at end of file