Добавлена ветка prod_prep

This commit is contained in:
grigorij
2026-07-31 12:58:23 +04:00
parent bf26370da9
commit f289631645
36 changed files with 893 additions and 290 deletions
+112 -18
View File
@@ -1,12 +1,14 @@
"""Booking handler - full FSM flow for service reservation."""
import re
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram.types import Message, CallbackQuery
from aiogram.types import Message, CallbackQuery, FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton
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.config import SERVICES as ALL_SERVICES, ADMIN_CHAT_ID
from app.services.booking_service import (
create_booking,
update_booking,
@@ -32,6 +34,17 @@ def _services_to_str(data):
return main
def _make_service_card(key):
"""Recreate the service card text for 'back' navigation from a photo card."""
svc = ALL_SERVICES[key]
return (
svc["emoji"] + " " + svc["name"] + "\n\n"
+ svc["desc"] + "\n\n"
+ "\u23f1 Продолжительность: " + str(svc["duration"]) + " мин\n"
+ "\U0001f4b0 Цена: " + str(svc["price"]) + "\u20bd"
)
# отмена записи на любом шаге
@router.message(F.text.lower() == "отмена")
async def cancel(message: Message, state: FSMContext):
@@ -53,15 +66,14 @@ async def start_booking(message: Message, state: FSMContext):
)
# выбор первой услуги
# выбор первой услуги (через кнопку "📅 Записаться")
@router.callback_query(
StateFilter(BookingState.service),
F.data.startswith("service_")
)
async def choose_service(callback: CallbackQuery, state: FSMContext):
from app.config import SERVICES
service_name = SERVICES[callback.data]
svc_data = ALL_SERVICES[callback.data]
service_name = svc_data["name"] if isinstance(svc_data, dict) else svc_data
logger.debug("User {uid} selected first service: {srv}", uid=callback.from_user.id, srv=service_name)
await state.update_data(service=service_name, extra_services=[])
@@ -82,10 +94,7 @@ async def choose_service(callback: CallbackQuery, state: FSMContext):
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(
"👤 Выберите специалиста:",
@@ -94,12 +103,12 @@ async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
await callback.answer()
return
# Toggle extra service
actual_key = callback.data.replace("extra_", "", 1)
service_name = SERVICES.get(actual_key)
if not service_name:
svc_data = ALL_SERVICES.get(actual_key)
if not svc_data:
await callback.answer("Услуга не найдена")
return
service_name = svc_data["name"] if isinstance(svc_data, dict) else svc_data
data = await state.get_data()
extra = data.get("extra_services", [])
@@ -133,7 +142,8 @@ async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
async def choose_master(callback: CallbackQuery, state: FSMContext):
from app.config import MASTERS
master_name = MASTERS[callback.data]
master_data = MASTERS[callback.data]
master_name = master_data["name"] if isinstance(master_data, dict) else master_data
logger.debug("User {uid} selected master: {m}", uid=callback.from_user.id, m=master_name)
await state.update_data(master=master_name)
@@ -222,6 +232,14 @@ async def confirm_booking_callback(callback: CallbackQuery, state: FSMContext):
date = data.get("date", "?")
time = data.get("time", "?")
from app.db.postgres import PostgresDB as _PG
from app.config import USE_POSTGRES as _UPG
if _UPG:
_u = await _PG.get_user(callback.from_user.id)
if not _u:
_n = callback.from_user.full_name or callback.from_user.username or str(callback.from_user.id)
await _PG.set_user(callback.from_user.id, _n)
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("❌ Это время уже занято. Пожалуйста, выберите другое время.")
@@ -240,6 +258,26 @@ async def confirm_booking_callback(callback: CallbackQuery, state: FSMContext):
cleanup_cancelled()
result_text = "✅ Запись успешно создана"
# Notify admin about new booking
try:
if ADMIN_CHAT_ID:
from aiogram import Bot
from app.config import BOT_TOKEN
admin_bot = Bot(token=BOT_TOKEN)
user_str = callback.from_user.full_name or callback.from_user.username or str(callback.from_user.id)
await admin_bot.send_message(
ADMIN_CHAT_ID,
"🆕 Новая запись!\n\n"
f"👤 Клиент: {user_str} (id={callback.from_user.id})\n"
f"💆 Услуги: {services_str}\n"
f"👤 Мастер: {master}\n"
f"📅 Дата: {date}\n"
f"🕒 Время: {time}"
)
await admin_bot.session.close()
except Exception as e:
logger.warning("Failed to notify admin: {error}", error=e)
await state.clear()
await callback.message.edit_text(result_text)
await callback.answer()
@@ -271,24 +309,80 @@ async def edit_booking_callback(callback: CallbackQuery, state: FSMContext):
)
# прямая запись с карточки услуги (без предварительного выбора)
@router.callback_query(F.data.startswith("service_"))
async def quick_book_service(callback: CallbackQuery, state: FSMContext):
svc_data = ALL_SERVICES[callback.data]
service_name = svc_data["name"] if isinstance(svc_data, dict) else svc_data
svc_key = callback.data
logger.info("User {uid} quick book from card: {srv}", uid=callback.from_user.id, srv=service_name)
await state.clear()
await state.update_data(
service=service_name,
extra_services=[],
_from_card=True,
_card_key=svc_key
)
await state.set_state(BookingState.extra_services)
text = (
"✅ Выбрано: " + service_name + "\n\n"
"Хотите добавить ещё услугу?\n"
"Нажмите нужную или «Готово»:"
)
# Карточка с фото — удаляем и отправляем новое сообщение
try:
await callback.message.delete()
await callback.message.answer(text, reply_markup=get_extra_services_keyboard([]))
except Exception:
await callback.message.edit_text(text, reply_markup=get_extra_services_keyboard([]))
await callback.answer()
@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.extra_services:
# Back to first service
# Назад к списку услуг — проверяем, с карточки пришёл или из меню
await state.set_state(BookingState.service)
data = await state.get_data()
data.pop("extra_services", None)
data.pop("service", None)
from_card = data.pop("_from_card", False)
card_key = data.pop("_card_key", None)
await state.set_data(data)
await callback.message.edit_text(
"Выберите услугу:",
reply_markup=get_services_keyboard()
)
if from_card and card_key and card_key in ALL_SERVICES:
# Возвращаем карточку с фото
svc = ALL_SERVICES[card_key]
card_text = _make_service_card(card_key)
book_btn = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text="\U0001f4c5 Записаться", callback_data=card_key)
]])
try:
img = FSInputFile(svc["image"])
await callback.message.answer_photo(photo=img, caption=card_text, reply_markup=book_btn)
try:
await callback.message.delete()
except Exception:
pass
except Exception:
await callback.message.edit_text(
"Выберите услугу:",
reply_markup=get_services_keyboard()
)
else:
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", [])