Добавлена ветка 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
+16 -5
View File
@@ -1,12 +1,23 @@
"""About handler - salon info page."""
from aiogram import Router, F
from aiogram.types import Message
from app.logger import logger
from aiogram.fsm.context import FSMContext
from app.logger import logger
router = Router()
@router.message(F.text == "О нас")
async def about(message: Message, state: FSMContext):
await state.clear() # 🔥 СБРОС FSM
await message.answer("Мы demo SPA бот 💆‍♂️")
@router.message(F.text == "\u2139\ufe0f \u041e \u043d\u0430\u0441")
async def about(message: Message, state: FSMContext):
await state.clear()
logger.info("User {uid} opened About", uid=message.from_user.id)
text = (
"\u262f\ufe0f SPA-\u0441\u0430\u043b\u043e\u043d Demo\n\n"
"\u041c\u044b - \u0443\u044e\u0442\u043d\u044b\u0439 SPA-\u0441\u0430\u043b\u043e\u043d \u0432 \u0446\u0435\u043d\u0442\u0440\u0435 \u0433\u043e\u0440\u043e\u0434\u0430.\n\n"
"\U0001f4aa \u041f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u0430\u0441\u0442\u0435\u0440\u0430\n"
"\U0001f9ec \u041d\u0430\u0442\u0443\u0440\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u0441\u043c\u0435\u0442\u0438\u043a\u0430\n"
"\U0001f3e1 \u0423\u044e\u0442\u043d\u0430\u044f \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u0430\n\n"
"\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e \u0441 09:00 \u0434\u043e 19:00\n\n"
"\U0001f447 \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u00bb \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f!"
)
await message.answer(text)
+67 -28
View File
@@ -1,20 +1,28 @@
"""Admin handler for spa-telegram-bot — secure with password."""
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import Command, CommandObject
from app.db.memory import load_bookings
from app.config import ADMIN_PASSWORD
from aiogram import Bot
from app.config import USE_POSTGRES
from app.config import ADMIN_PASSWORD, BOT_TOKEN
from app.logger import logger
router = Router()
ADMIN_IDS = {991309145}
# Store approved sessions — user_ids that passed password check
_approved = set()
_notify_bot = None
def _get_bot():
global _notify_bot
if _notify_bot is None:
_notify_bot = Bot(token=BOT_TOKEN)
return _notify_bot
def is_admin(user_id: int, password: str | None = None) -> bool:
"""Check if user is admin: by user_id, by password, or by approved session."""
if user_id in ADMIN_IDS:
return True
if user_id in _approved:
@@ -30,52 +38,83 @@ async def admin_panel(message: Message, command: CommandObject):
uid = message.from_user.id
password = command.args
if not is_admin(uid, password=password):
return # Silent
if not is_admin(uid, password):
return
logger.info("Admin {uid} opened admin panel", uid=uid)
bookings = load_bookings()
active = [b for b in bookings if b.get("status") == "active"]
if USE_POSTGRES:
from app.db.postgres import PostgresDB
active = await PostgresDB.get_all_active_bookings()
else:
from app.db.memory import load_bookings
bookings = load_bookings()
active = [b for b in bookings if b.get("status") == "active"]
if not active:
await message.answer("📋 Активных записей нет")
await message.answer("\U0001f4cb \u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u043d\u0435\u0442")
return
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
for booking in active:
buttons = [[
InlineKeyboardButton(
text="❌ Отменить",
text="\u274c \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c",
callback_data=f"admin_cancel_{booking['id']}"
)
]]
user_name = booking.get("user_name") or f"id{booking['user_id']}"
await message.answer(
f"📋 Запись #{booking['id']}\n"
f"👤 Клиент: {user_name}\n"
f"💆 Услуги: {booking.get('service', '?')}\n"
f"👤 Мастер: {booking.get('master', '?')}\n"
f"📅 Дата: {booking.get('date', '?')}\n"
f"🕒 Время: {booking.get('time', '?')}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
text = (
"\U0001f4cb \u0417\u0430\u043f\u0438\u0441\u044c #" + str(booking['id']) + "\n"
"\U0001f464 \u041a\u043b\u0438\u0435\u043d\u0442: " + user_name + "\n"
"\U0001f486 \u0423\u0441\u043b\u0443\u0433\u0430: " + booking.get('service', '?') + "\n"
"\U0001f464 \u041c\u0430\u0441\u0442\u0435\u0440: " + booking.get('master', '?') + "\n"
"\U0001f4c5 \u0414\u0430\u0442\u0430: " + booking.get('date', '?') + "\n"
"\U0001f552 \u0412\u0440\u0435\u043c\u044f: " + booking.get('time', '?')
)
await message.answer(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@router.callback_query(F.data.startswith("admin_cancel_"))
async def admin_cancel_booking(callback: CallbackQuery):
if not is_admin(callback.from_user.id):
await callback.answer("⛔ Нет доступа")
await callback.answer("\u26d4 \u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430")
return
booking_id = int(callback.data.split("_")[2])
logger.info("Admin {uid} cancelled booking #{bid}", uid=callback.from_user.id, bid=booking_id)
from app.db.memory import cancel_booking
cancel_booking(booking_id)
if USE_POSTGRES:
from app.db.postgres import PostgresDB
success = await PostgresDB.cancel_booking(booking_id)
booking = await PostgresDB.get_booking_by_id(booking_id) if success else None
else:
from app.db.memory import cancel_booking as json_cancel, load_bookings
success = json_cancel(booking_id)
bookings = load_bookings()
booking = next((b for b in bookings if b["id"] == booking_id), None)
await callback.message.edit_text(
f"✅ Запись #{booking_id} отменена админом"
)
await callback.answer("Запись отменена")
if not success:
await callback.message.edit_text("\u0417\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0438\u043b\u0438 \u0443\u0436\u0435 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430")
await callback.answer()
return
await callback.message.edit_text("\u2705 \u0417\u0430\u043f\u0438\u0441\u044c #" + str(booking_id) + " \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u043e\u043c")
if booking:
try:
bot = _get_bot()
user_id = booking["user_id"]
notify_text = (
"\u26a0\ufe0f \u0412\u0430\u0448\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c.\n\n"
"\U0001f486 \u0423\u0441\u043b\u0443\u0433\u0430: " + booking.get('service', '?') + "\n"
"\U0001f464 \u041c\u0430\u0441\u0442\u0435\u0440: " + booking.get('master', '?') + "\n"
"\U0001f4c5 \u0414\u0430\u0442\u0430: " + booking.get('date', '?') + "\n"
"\U0001f552 \u0412\u0440\u0435\u043c\u044f: " + booking.get('time', '?') + "\n\n"
"\U0001f4cb \u0427\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f \u0441\u043d\u043e\u0432\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f\u00bb"
)
await bot.send_message(user_id, notify_text)
logger.info("Notified user {uid} about cancellation", uid=user_id)
except Exception as e:
logger.warning("Failed to notify user: {err}", err=e)
await callback.answer()
+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", [])
+15 -4
View File
@@ -1,12 +1,23 @@
"""Contacts handler - address, phone, social."""
from aiogram import Router, F
from aiogram.types import Message
from app.logger import logger
from aiogram.fsm.context import FSMContext
from app.logger import logger
router = Router()
@router.message(F.text == "📞 Контакты")
@router.message(F.text == "\U0001f4de \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b")
async def contacts(message: Message, state: FSMContext):
await state.clear()
await message.answer("📍 Москва\n☎ +7 999 999 99 99")
logger.info("User {uid} requested contacts", uid=message.from_user.id)
text = (
"\U0001f4de \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b\n\n"
"\U0001f4cd \u0433. \u041c\u043e\u0441\u043a\u0432\u0430, \u0443\u043b. \u0426\u0432\u0435\u0442\u043e\u0447\u043d\u0430\u044f, 15\n\n"
"\U0001f4de +7 (999) 123-45-67\n\n"
"\U0001f4e7 info@spa-demo.ru\n\n"
"\U0001f310 @spa_demo_salon\n\n"
"\u0420\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b:\n"
"\u041f\u043d-\u0412\u0441: 09:00 - 19:00"
)
await message.answer(text)
+63 -56
View File
@@ -1,88 +1,95 @@
"""My bookings handler - view, edit, cancel bookings."""
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
from app.db.memory import get_user_bookings, cancel_booking, update_booking
from app.keyboards.menu import menu
from app.config import USE_POSTGRES
from app.services.booking_service import get_user_bookings, cancel_booking
from app.keyboards.my_booking import get_booking_actions
from app.keyboards.services import get_services_keyboard
from app.states.booking import BookingState
from app.logger import logger
router = Router()
@router.message(
F.text == "📄 Мои записи"
)
async def my_bookings(
message: Message
):
@router.message(F.text == "\U0001f4c4 \u041c\u043e\u0438 \u0437\u0430\u043f\u0438\u0441\u0438")
async def my_bookings(message: Message):
logger.info("User {uid} opened My Bookings", uid=message.from_user.id)
try:
bookings = get_user_bookings(
message.from_user.id
)
bookings = await get_user_bookings(message.from_user.id)
logger.debug("Found {n} bookings", n=len(bookings))
if not bookings:
await message.answer(
"У вас нет активных записей"
)
await message.answer("У вас нет активных записей")
return
for booking in bookings:
svc = booking["service"]
mst = booking["master"]
dat = booking["date"]
tim = booking["time"]
text = "💆 " + svc + "\n"
text += "👤 " + mst + "\n"
text += "📅 " + dat + "\n"
text += "🕒 " + tim
await message.answer(
text,
reply_markup=get_booking_actions(
booking["id"]
)
# Group all bookings into one message
lines = []
for b in bookings:
bid = b["id"]
svc = b["service"]
mst = b["master"]
dat = b["date"]
tim = b["time"]
lines.append(
f"#{bid} \U0001f486 {svc}\n"
f"\U0001f464 {mst}\n"
f"\U0001f4c5 {dat}\U0001f552 {tim}"
)
full_text = "\u0423 \u0432\u0430\u0441 " + str(len(bookings)) + " \u0437\u0430\u043f\u0438\u0441\u0438:\n\n" + "\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n".join(lines)
await message.answer(full_text)
# Send action buttons for each booking
for b in bookings:
await message.answer(
f"#{b['id']}: {b['service']}",
reply_markup=get_booking_actions(b["id"])
)
except Exception as e:
logger.exception("Error in my_bookings: {error}", error=e)
await message.answer(
"Произошла ошибка. Попробуйте позже."
)
await message.answer("Произошла ошибка. Попробуйте позже.")
@router.callback_query(
F.data.startswith("cancel_booking_")
)
@router.callback_query(F.data.startswith("cancel_booking_"))
async def cancel_booking_handler(callback: CallbackQuery):
booking_id = int(callback.data.split("_")[2])
logger.info("User {uid} cancelled booking #{bid}", uid=callback.from_user.id, bid=booking_id)
cancel_booking(
booking_id
)
await callback.message.edit_text(
"Запись отменена"
)
await cancel_booking(booking_id)
await callback.message.edit_text("\u2705 Запись отменена")
await callback.answer()
@router.callback_query(
F.data.startswith("edit_booking_")
)
async def edit_booking_handler(
callback: CallbackQuery,
state: FSMContext
):
@router.callback_query(F.data.startswith("edit_booking_"))
async def edit_booking_handler(callback: CallbackQuery, state: FSMContext):
booking_id = int(callback.data.split("_")[2])
update_booking(
booking_id=booking_id,
service="",
master="",
date="",
time=""
)
if USE_POSTGRES:
from app.db.postgres import PostgresDB
booking = await PostgresDB.get_booking_by_id(booking_id)
else:
from app.db.memory import load_bookings
bookings = load_bookings()
booking = next((b for b in bookings if b["id"] == booking_id), None)
await callback.answer(
"Редактирование не поддерживается"
if not booking:
await callback.message.edit_text("Запись не найдена")
await callback.answer()
return
await state.set_data({
"editing_booking_id": booking_id,
"service": booking.get("service", ""),
"master": booking.get("master", ""),
"date": booking.get("date", ""),
"time": booking.get("time", ""),
})
await state.set_state(BookingState.service)
await callback.message.edit_text(
"\u270f\ufe0f Редактирование записи #" + str(booking_id) + "\n\nВыберите новую услугу:",
reply_markup=get_services_keyboard()
)
await callback.answer()
+23 -7
View File
@@ -1,18 +1,34 @@
"""User profile handler - auto-registers if missing."""
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from app.db.memory import get_user
from app.db.postgres import PostgresDB
from app.config import USE_POSTGRES
from app.logger import logger
router = Router()
@router.message(F.text == "👤 Профиль")
@router.message(F.text == "\U0001f464 \u041f\u0440\u043e\u0444\u0438\u043b\u044c")
async def profile(message: Message, state: FSMContext):
await state.clear()
user = get_user(message.from_user.id)
if not user:
await message.answer("Профиль не найден")
return
await message.answer(f"👤 Имя: {user['name']}")
user_id = message.from_user.id
name = message.from_user.full_name or "\u0413\u043e\u0441\u0442\u044c"
if USE_POSTGRES:
user = await PostgresDB.get_user(user_id)
if not user:
logger.info("Auto-registering user {uid} in PG", uid=user_id)
await PostgresDB.set_user(user_id, name)
user = await PostgresDB.get_user(user_id)
user_name = user.get("name") or user.get("full_name", "\u0413\u043e\u0441\u0442\u044c")
else:
from app.db.memory import get_user, set_user
user = get_user(user_id)
if not user:
set_user(user_id, {"name": name})
user = get_user(user_id)
user_name = user.get("name", "\u0413\u043e\u0441\u0442\u044c")
await message.answer(f"\U0001f464 \u041f\u0440\u043e\u0444\u0438\u043b\u044c\n\n\U0001f464 \u0418\u043c\u044f: {user_name}")
+29 -11
View File
@@ -1,23 +1,41 @@
"""Services handler - show rich service cards with images."""
from aiogram import Router, F
from aiogram.types import Message
from aiogram.types import Message, FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram import Bot
from app.keyboards.menu import menu
from app.keyboards.services import get_services_keyboard
from app.config import SERVICES, BOT_TOKEN
from app.logger import logger
router = Router()
_bot = None
def bot():
global _bot
if _bot is None:
_bot = Bot(token=BOT_TOKEN)
return _bot
@router.message(F.text == "\U0001f4cb \u0423\u0441\u043b\u0443\u0433\u0438")
async def show_services(message: Message, state: FSMContext):
await state.clear()
logger.info("User {uid} requested services list", uid=message.from_user.id)
await message.answer(
"\U0001f4cb \u041d\u0430\u0448\u0438 \u0443\u0441\u043b\u0443\u0433\u0438:\n\n"
"\U0001f4aa \u041c\u0430\u0441\u0441\u0430\u0436 - \u0440\u0430\u0441\u0441\u043b\u0430\u0431\u043b\u044f\u044e\u0449\u0438\u0439 \u0438 \u043b\u0435\u0447\u0435\u0431\u043d\u044b\u0439\n"
"\U0001f388 SPA - \u043a\u043e\u043c\u043f\u043b\u0435\u043a\u0441\u043d\u044b\u0435 \u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n"
"\U0001f484 \u041a\u043e\u0441\u043c\u0435\u0442\u043e\u043b\u043e\u0433\u0438\u044f - \u0443\u0445\u043e\u0434 \u0437\u0430 \u043b\u0438\u0446\u043e\u043c\n\n"
"\u0427\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f\u00bb",
reply_markup=get_services_keyboard()
)
for key, svc in SERVICES.items():
try:
card = (
svc["emoji"] + " " + svc["name"] + "\n\n"
+ svc["desc"] + "\n\n"
+ "\u23f1 \u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c: " + str(svc["duration"]) + " \u043c\u0438\u043d\n"
+ "\U0001f4b0 \u0426\u0435\u043d\u0430: " + str(svc["price"]) + "\u20bd"
)
book_btn = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text="\U0001f4c5 \u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f", callback_data=key)
]])
img = FSInputFile(svc["image"])
await bot().send_photo(message.chat.id, photo=img, caption=card, reply_markup=book_btn)
except Exception as e:
logger.warning("Error sending service card for {key}: {err}", key=key, err=e)
+24 -7
View File
@@ -1,9 +1,11 @@
"""Start handler with welcome greeting and prices."""
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message
from aiogram.types import Message, FSInputFile
from app.keyboards.menu import menu
from app.db.memory import get_user, set_user
from app.db.postgres import PostgresDB
from app.config import SERVICES
from app.logger import logger
router = Router()
@@ -12,10 +14,25 @@ router = Router()
@router.message(CommandStart())
async def start(message: Message):
user_id = message.from_user.id
if not get_user(user_id):
set_user(user_id, {"name": message.from_user.full_name})
name = message.from_user.full_name or "\u0413\u043e\u0441\u0442\u044c"
from app.config import USE_POSTGRES
if USE_POSTGRES:
await PostgresDB.set_user(user_id, name)
else:
from app.db.memory import get_user, set_user
if not get_user(user_id):
set_user(user_id, {"name": name})
await message.answer(
"👋 Добро пожаловать в Demo Bot",
reply_markup=menu,
greet = (
"\U0001f44b \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c, " + name + "!\n\n"
"\u262f\ufe0f SPA-\u0441\u0430\u043b\u043e\u043d Demo - \u0432\u0430\u0448 \u043e\u0441\u0442\u0440\u043e\u0432\u043e\u043a \u0440\u0435\u043b\u0430\u043a\u0441\u0430\u0446\u0438\u0438\n\n"
"\U0001f4aa \u041c\u0430\u0441\u0441\u0430\u0436 - \u043e\u0442 " + str(SERVICES["service_massage"]["price"]) + "\u20bd\n"
"\U0001f388 SPA-\u043a\u043e\u043c\u043f\u043b\u0435\u043a\u0441 - \u043e\u0442 " + str(SERVICES["service_spa"]["price"]) + "\u20bd\n"
"\U0001f484 \u041a\u043e\u0441\u043c\u0435\u0442\u043e\u043b\u043e\u0433\u0438\u044f - \u043e\u0442 " + str(SERVICES["service_cosmetology"]["price"]) + "\u20bd\n\n"
"\U0001f447 \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u00bb \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044c"
)
try:
img = FSInputFile("assets/welcome.png")
await message.answer_photo(photo=img, caption=greet, reply_markup=menu)
except Exception:
await message.answer(greet, reply_markup=menu)