Выбор нескольких услуг в одну запись: мультивыбор на шаге услуг, кнопка Готово
This commit is contained in:
+128
-150
@@ -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 callback.message.edit_text(
|
||||
"✅ Выбрано: " + service_name + "\n\n"
|
||||
"Хотите добавить ещё услугу?\n"
|
||||
"Нажмите нужную или «Готово»:",
|
||||
reply_markup=get_extra_services_keyboard()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
await state.set_state(
|
||||
BookingState.master
|
||||
)
|
||||
|
||||
# выбор дополнительных услуг
|
||||
@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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user