diff --git a/app/handlers/booking.py b/app/handlers/booking.py index e28fa37..09157af 100644 --- a/app/handlers/booking.py +++ b/app/handlers/booking.py @@ -4,7 +4,7 @@ from aiogram import Router, F from aiogram.fsm.context import FSMContext from aiogram.filters import StateFilter from aiogram.types import Message, CallbackQuery -from app.keyboards.services import get_services_keyboard +from app.keyboards.services import get_services_keyboard, get_extra_services_keyboard from app.keyboards.masters import get_masters_keyboard from app.states.booking import BookingState from app.services.booking_service import ( @@ -23,6 +23,15 @@ from app.logger import logger router = Router() +def _services_to_str(data): + """Combine main + extra services into one string.""" + main = data.get("service", "?") + extra = data.get("extra_services", []) + if extra: + return main + ", " + ", ".join(extra) + return main + + # отмена записи на любом шаге @router.message(F.text.lower() == "отмена") async def cancel(message: Message, state: FSMContext): @@ -37,72 +46,103 @@ async def cancel(message: Message, state: FSMContext): @router.message(F.text == "📅 Записаться") async def start_booking(message: Message, state: FSMContext): await state.clear() - await state.set_state(BookingState.service) - await message.answer( "Выберите услугу:", reply_markup=get_services_keyboard() ) +# выбор первой услуги @router.callback_query( StateFilter(BookingState.service), F.data.startswith("service_") ) -async def choose_service( - callback: CallbackQuery, - state: FSMContext -): +async def choose_service(callback: CallbackQuery, state: FSMContext): from app.config import SERVICES - services = SERVICES - service_name = services[callback.data] - logger.debug("User {uid} selected service: {srv}", uid=callback.from_user.id, srv=service_name) + service_name = SERVICES[callback.data] + logger.debug("User {uid} selected first service: {srv}", uid=callback.from_user.id, srv=service_name) - await state.update_data( - service=service_name - ) + await state.update_data(service=service_name, extra_services=[]) + await state.set_state(BookingState.extra_services) - await state.set_state( - BookingState.master - ) await callback.message.edit_text( - "👤 Выберите специалиста:", - reply_markup=get_masters_keyboard() + "✅ Выбрано: " + service_name + "\n\n" + "Хотите добавить ещё услугу?\n" + "Нажмите нужную или «Готово»:", + reply_markup=get_extra_services_keyboard() ) - await callback.answer() -#выбор мастера + +# выбор дополнительных услуг +@router.callback_query( + StateFilter(BookingState.extra_services), + F.data.startswith("extra_") +) +async def choose_extra_service(callback: CallbackQuery, state: FSMContext): + from app.config import SERVICES + + if callback.data == "extra_done": + # Go to master selection + await state.set_state(BookingState.master) + await callback.message.edit_text( + "👤 Выберите специалиста:", + reply_markup=get_masters_keyboard() + ) + await callback.answer() + return + + # Toggle extra service + actual_key = callback.data.replace("extra_", "", 1) + service_name = SERVICES.get(actual_key) + if not service_name: + await callback.answer("Услуга не найдена") + return + + data = await state.get_data() + extra = data.get("extra_services", []) + + if service_name in extra: + extra.remove(service_name) + logger.debug("User {uid} removed extra service: {srv}", uid=callback.from_user.id, srv=service_name) + else: + extra.append(service_name) + logger.debug("User {uid} added extra service: {srv}", uid=callback.from_user.id, srv=service_name) + + await state.update_data(extra_services=extra) + + data = await state.get_data() + services_text = _services_to_str(data) + + await callback.message.edit_text( + "✅ Выбрано: " + services_text + "\n\n" + "Хотите добавить ещё услугу?\n" + "Нажмите нужную или «Готово»:", + reply_markup=get_extra_services_keyboard(extra) + ) + await callback.answer() + + +# выбор мастера @router.callback_query( StateFilter(BookingState.master), F.data.startswith("master_") ) -async def choose_master( - callback: CallbackQuery, - state: FSMContext -): - +async def choose_master(callback: CallbackQuery, state: FSMContext): from app.config import MASTERS - masters = MASTERS - master_name = masters[callback.data] + master_name = MASTERS[callback.data] logger.debug("User {uid} selected master: {m}", uid=callback.from_user.id, m=master_name) - await state.update_data( - master=master_name - ) - - await state.set_state( - BookingState.date - ) + await state.update_data(master=master_name) + await state.set_state(BookingState.date) await callback.message.edit_text( "📅 Выберите дату:", reply_markup=get_dates_keyboard() ) - await callback.answer() @@ -111,74 +151,48 @@ async def choose_master( StateFilter(BookingState.date), F.data.startswith("date_") ) -async def choose_date( - callback: CallbackQuery, - state: FSMContext -): - selected_date = callback.data.replace( - "date_", - "" - ) +async def choose_date(callback: CallbackQuery, state: FSMContext): + selected_date = callback.data.replace("date_", "") - await state.update_data( - date=selected_date - ) + await state.update_data(date=selected_date) logger.debug("User {uid} selected date: {d}", uid=callback.from_user.id, d=selected_date) data = await state.get_data() + busy_times = await get_busy_times(master=data.get("master", "?"), date=selected_date) - busy_times = await get_busy_times( - master=data.get('master', '?'), - date=selected_date - ) - - await state.set_state( - BookingState.time - ) + await state.set_state(BookingState.time) await callback.message.edit_text( "🕒 Выберите время:", - reply_markup=get_time_keyboard( - busy_times - ) + reply_markup=get_time_keyboard(busy_times) ) - await callback.answer() +# выбор времени @router.callback_query( StateFilter(BookingState.time), F.data.startswith("time_") ) -async def choose_time( - callback: CallbackQuery, - state: FSMContext -): - selected_time = callback.data.replace( - "time_", - "" - ) +async def choose_time(callback: CallbackQuery, state: FSMContext): + selected_time = callback.data.replace("time_", "") - await state.update_data( - time=selected_time - ) + await state.update_data(time=selected_time) logger.debug("User {uid} selected time: {t}", uid=callback.from_user.id, t=selected_time) data = await state.get_data() + services_str = _services_to_str(data) - await state.set_state( - BookingState.confirm - ) + await state.set_state(BookingState.confirm) await callback.message.edit_text( - f"📋 Проверьте запись:\n\n" - f"💆 Услуга: {data.get('service', '?')}\n" + "📋 Проверьте запись:\n\n" + f"💆 Услуги: {services_str}\n" f"📅 Дата: {data.get('date', '?')}\n" - f"👤 Мастер:{data.get('master', '?')}\n" + f"👤 Мастер: {data.get('master', '?')}\n" f"🕒 Время: {selected_time}", reply_markup=get_confirm_booking_keyboard() ) - await callback.answer() @@ -187,63 +201,47 @@ async def choose_time( StateFilter(BookingState.confirm), F.data == "confirm_booking" ) -async def confirm_booking_callback( - callback: CallbackQuery, - state: FSMContext -): +async def confirm_booking_callback(callback: CallbackQuery, state: FSMContext): data = await state.get_data() logger.info("User {uid} confirmed booking: {data}", uid=callback.from_user.id, data=data) - editing_booking_id = data.get( - "editing_booking_id" - ) + services_str = _services_to_str(data) + editing_booking_id = data.get("editing_booking_id") if editing_booking_id: - await update_booking( booking_id=editing_booking_id, - service=data.get('service', '?'), - master=data.get('master', '?'), - date=data.get('date', '?'), - time=data.get('time', '?') + service=services_str, + master=data.get("master", "?"), + date=data.get("date", "?"), + time=data.get("time", "?") ) - result_text = "✅ Запись успешно обновлена" - else: - - # Check if slot is already taken - master = data.get('master', '?') - date = data.get('date', '?') - time = data.get('time', '?') + master = data.get("master", "?") + date = data.get("date", "?") + time = data.get("time", "?") if await is_slot_busy(master, date, time): logger.warning("Slot already busy: {mst} {dt} {tm}", mst=master, dt=date, tm=time) - await callback.message.edit_text( - "❌ Это время уже занято. Пожалуйста, выберите другое время." - ) + await callback.message.edit_text("❌ Это время уже занято. Пожалуйста, выберите другое время.") await state.clear() await callback.answer() return await create_booking( user_id=callback.from_user.id, - service=data.get('service', '?'), + service=services_str, master=master, date=date, - time=time + time=time, + user_name=callback.from_user.full_name or callback.from_user.username or str(callback.from_user.id) ) - - # Clean up old cancelled bookings cleanup_cancelled() - result_text = "✅ Запись успешно создана" await state.clear() - - await callback.message.edit_text( - result_text - ) + await callback.message.edit_text(result_text) await callback.answer() @@ -251,16 +249,9 @@ async def confirm_booking_callback( StateFilter(BookingState.confirm), F.data == "cancel_booking" ) -async def cancel_booking_callback( - callback: CallbackQuery, - state: FSMContext -): +async def cancel_booking_callback(callback: CallbackQuery, state: FSMContext): await state.clear() - - await callback.message.edit_text( - "❌ Запись отменена" - ) - + await callback.message.edit_text("❌ Запись отменена") await callback.answer() @@ -268,23 +259,11 @@ async def cancel_booking_callback( StateFilter(BookingState.confirm), F.data == "edit_booking" ) -async def edit_booking_callback( - callback: CallbackQuery, - state: FSMContext -): +async def edit_booking_callback(callback: CallbackQuery, state: FSMContext): old_data = await state.get_data() - await state.clear() - - await state.update_data( - service=old_data.get("service") - ) - - await state.set_state( - BookingState.service - ) - - from app.keyboards.services import get_services_keyboard + await state.update_data(service=old_data.get("service")) + await state.set_state(BookingState.service) await callback.message.edit_text( "Выберите услугу:", @@ -292,29 +271,37 @@ async def edit_booking_callback( ) -@router.callback_query( - F.data == "go_back" -) -async def go_back_handler( - callback: CallbackQuery, - state: FSMContext -): +@router.callback_query(F.data == "go_back") +async def go_back_handler(callback: CallbackQuery, state: FSMContext): current_state = await state.get_state() - if current_state == BookingState.master: - # Back to service selection + if current_state == BookingState.extra_services: + # Back to first service await state.set_state(BookingState.service) - from app.keyboards.services import get_services_keyboard + data = await state.get_data() + data.pop("extra_services", None) + data.pop("service", None) + await state.set_data(data) await callback.message.edit_text( "Выберите услугу:", reply_markup=get_services_keyboard() ) + elif current_state == BookingState.master: + # Back to extra services + await state.set_state(BookingState.extra_services) + data = await state.get_data() + extra = data.get("extra_services", []) + services_text = _services_to_str(data) + await callback.message.edit_text( + "✅ Выбрано: " + services_text + "\n\n" + "Хотите добавить ещё услугу?\n" + "Нажмите нужную или «Готово»:", + reply_markup=get_extra_services_keyboard(extra) + ) + elif current_state == BookingState.date: - # Back to master selection await state.set_state(BookingState.master) - from app.keyboards.masters import get_masters_keyboard - # Remove saved date from state, keep service data = await state.get_data() data.pop("date", None) data.pop("time", None) @@ -325,10 +312,7 @@ async def go_back_handler( ) elif current_state == BookingState.time: - # Back to date selection await state.set_state(BookingState.date) - from app.keyboards.calendar import get_dates_keyboard - # Remove saved time, keep master/date data = await state.get_data() data.pop("time", None) await state.set_data(data) @@ -338,11 +322,8 @@ async def go_back_handler( ) elif current_state == BookingState.confirm: - # Back to time selection await state.set_state(BookingState.time) data = await state.get_data() - from app.keyboards.time_slots import get_time_keyboard - from app.services.booking_service import get_busy_times busy_times = await get_busy_times( master=data.get("master", "?"), date=data.get("date", "?") @@ -353,10 +334,7 @@ async def go_back_handler( ) else: - # Fallback — back to start await state.clear() - await callback.message.edit_text( - "Отмена записи" - ) + await callback.message.edit_text("Отмена записи") await callback.answer() diff --git a/app/keyboards/services.py b/app/keyboards/services.py index 468f9ca..c1df4fe 100644 --- a/app/keyboards/services.py +++ b/app/keyboards/services.py @@ -12,3 +12,22 @@ def get_services_keyboard(): ] buttons.append(get_back_button()) return InlineKeyboardMarkup(inline_keyboard=buttons) + + +def get_extra_services_keyboard(already_selected: list[str] | None = None): + """Keyboard for selecting extra services. Shows ticked/unticked.""" + if already_selected is None: + already_selected = [] + + buttons = [] + for key, name in SERVICES.items(): + if name in already_selected: + label = "\u2705 " + name + else: + label = name + buttons.append([InlineKeyboardButton(text=label, callback_data="extra_" + key)]) + + # Done button + buttons.append([InlineKeyboardButton(text="\u2705 \u0413\u043e\u0442\u043e\u0432\u043e", callback_data="extra_done")]) + buttons.append(get_back_button()) + return InlineKeyboardMarkup(inline_keyboard=buttons) diff --git a/app/states/booking.py b/app/states/booking.py index 6b5a03a..6019f0e 100644 --- a/app/states/booking.py +++ b/app/states/booking.py @@ -1,7 +1,8 @@ from aiogram.fsm.state import StatesGroup, State class BookingState(StatesGroup): - service = State() + service = State() # первая услуга + extra_services = State() # дополнительные услуги master = State() date = State() time = State()