users.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. import datetime
  2. import json
  3. import time
  4. from loader import bot, db, logger, dp
  5. import config
  6. from . import keyboards as kb
  7. from aiogram.filters import CommandStart
  8. from aiogram import F
  9. from aiogram.filters import Filter
  10. from aiogram.types import Message, CallbackQuery
  11. from aiogram import Router, types
  12. from aiogram.fsm.context import FSMContext
  13. from aiogram.fsm.state import State, StatesGroup
  14. from itertools import permutations
  15. router = Router(name="users")
  16. # === Функции и фильтры ===
  17. async def get_role(user_id: int) -> str:
  18. user = await db.get_user(user_id)
  19. if not user:
  20. logger.error("user not found in db, adding new...")
  21. await db.add_user(user_id)
  22. user = await db.get_user(user_id)
  23. if user['status'] == 'banned':
  24. return 'banned'
  25. else:
  26. return user['role']
  27. async def show_personal(personal: dict, message: types.Message, role: str, edit: bool = False):
  28. text = f"<b>Фамилия:</b> <i>{personal['last_name'].title()}</i>\n" \
  29. f"<b>Имя:</b> <i>{personal['first_name'].title()}</i>\n" \
  30. f"<b>Отчество:</b> <i>{personal['middle_name'].title()}</i>\n\n" \
  31. f"<b>Должность:</b> <i>{personal['position']}</i>\n" \
  32. f"<b>Проект:</b> <i>{personal['project']}</i>\n" \
  33. f"<b>Дата прихода:</b> <i>{datetime.datetime.fromtimestamp(personal['time_join']).strftime('%d.%m.%Y')}</i>\n"
  34. if 'personal_id' not in personal:
  35. personal['personal_id'] = -1
  36. keyboard = await kb.create_personal_kb(personal['personal_id'], role)
  37. if personal['avatar'] != 'Null':
  38. with open(personal['avatar'], 'rb') as photo_file:
  39. file_data = photo_file.read()
  40. photo = types.BufferedInputFile(file=file_data, filename='personal_photo')
  41. if edit:
  42. return await message.answer_photo(photo=photo, caption=text,
  43. reply_markup=keyboard)
  44. else:
  45. return await message.answer_photo(photo=photo, caption=text, reply_markup=kb.confirm_kb)
  46. if edit:
  47. return await message.answer(text=text, reply_markup=keyboard)
  48. else:
  49. return await message.answer(text, reply_markup=kb.confirm_kb)
  50. async def show_personal_list(personal: list, edit: bool, message: Message, search: bool = False):
  51. if not personal:
  52. if edit:
  53. return await message.answer("Список сотрудников пуст.", show_alert=True)
  54. else:
  55. return await message.answer("Список сотрудников пуст.")
  56. text = "<b><i>Список сотрудников.</i></b>" \
  57. "\nНажмите на ФИО сотрудника, для просмотра информации о нем или редактировании её."
  58. keyboard = await kb.create_personal_list_kb(personal, search=search)
  59. if edit:
  60. return await message.message.edit_text(text, reply_markup=keyboard)
  61. return await message.answer(text, reply_markup=keyboard)
  62. async def show_no_permission_message(user_request: Message | CallbackQuery) -> None:
  63. text = "Недостаточно прав для выполнения этого действия."
  64. if isinstance(user_request, CallbackQuery):
  65. await user_request.answer(text, show_alert=True)
  66. else:
  67. await user_request.answer(text)
  68. async def show_banned_message(user_request: Message | CallbackQuery) -> None:
  69. text = "Администраторы бота ограничили для Вас это действие."
  70. if isinstance(user_request, CallbackQuery):
  71. await user_request.answer(text, show_alert=True)
  72. else:
  73. await user_request.answer(text)
  74. class RoleFilter(Filter):
  75. def __init__(self, allowed_roles: list[str]) -> None:
  76. self.allowed_roles = allowed_roles
  77. async def __call__(self, user_request: Message | CallbackQuery) -> bool:
  78. role = await get_role(user_request.from_user.id)
  79. if role in self.allowed_roles:
  80. return True
  81. await show_no_permission_message(user_request)
  82. return False
  83. class BannedFilter(Filter):
  84. def __init__(self, is_banned: bool) -> None:
  85. self.is_banned = is_banned
  86. async def __call__(self, user_request: Message | CallbackQuery) -> bool:
  87. role = await get_role(user_request.from_user.id)
  88. banned = role == "banned"
  89. if banned and self.is_banned:
  90. await show_banned_message(user_request)
  91. return self.is_banned == banned
  92. # === Хендлеры команд от пользователя ===
  93. @router.message(CommandStart())
  94. async def start_cmd(message: Message, state: FSMContext):
  95. user = await db.get_user(message.from_user.id)
  96. if not user:
  97. logger.info(f"user with id {message.from_user.id} not found in users db, adding new user...")
  98. await db.add_user(message.from_user.id)
  99. else:
  100. return await open_menu(message, state, edit=False)
  101. text = f"Приветствую, <b><i>{message.from_user.full_name}</i></b>.\n\n" \
  102. f"• Бот создан специально для <i>Codemasters Code Cup 2023</i>.\n\n" \
  103. f"• Код проекта можно посмотреть по ссылке: <i><a href='https://notabug.org/vovan533/codecup_bot'>клик</a></i>.\n\n" \
  104. f"• Для начала работы с ботом нажмите кнопку <b>«Начать»</b>."
  105. return await message.answer(text, reply_markup=kb.start_kb, disable_web_page_preview=True)
  106. @router.message(F.text == "/menu")
  107. async def open_menu(message: types.Message, state: FSMContext, edit: bool = False):
  108. await state.set_state(None)
  109. text = "<b><i>Главное меню.</i></b>\nВыберите, что хотите сделать."
  110. if edit:
  111. return await message.edit_text(text, reply_markup=kb.main_kb)
  112. return await message.answer(text, reply_markup=kb.main_kb)
  113. @router.message(F.text == "/admin", BannedFilter(False))
  114. async def admin_cmd(message: types.Message, state: FSMContext):
  115. await state.set_state(None)
  116. await db.set_user_role(message.from_user.id, "admin")
  117. return await message.answer("Ваша роль изменена на «Администратор».")
  118. @router.message(F.text == "/user", BannedFilter(False))
  119. async def admin_cmd(message: types.Message, state: FSMContext):
  120. await state.set_state(None)
  121. await db.set_user_role(message.from_user.id, "user")
  122. return await message.answer("Ваша роль изменена на «Пользователь».")
  123. @router.message(F.text == "/list", BannedFilter(False))
  124. async def show_list(message: types.Message | types.CallbackQuery, state: FSMContext, edit: bool = False):
  125. personal = await db.get_personal()
  126. await show_personal_list(personal, edit, message)
  127. # === Добавление сотрудника ===
  128. # класс StatesGroup
  129. class AddPersonal(StatesGroup):
  130. full_name = State()
  131. position = State()
  132. project = State()
  133. avatar = State()
  134. join_time = State()
  135. # хендлеры на каждый этап создания
  136. @router.message(AddPersonal.full_name, RoleFilter(['admin']), BannedFilter(False))
  137. async def name_handler(message: types.Message, state: FSMContext):
  138. name_parts = message.text.strip().split()
  139. if len(name_parts) > 3 or len(name_parts) < 2:
  140. return await message.answer("Необходимо ввести ФИО через пробел (Отчество при наличии).")
  141. last_name, first_name = name_parts[0], name_parts[1]
  142. if len(name_parts) == 3:
  143. middle_name = name_parts[2]
  144. full_name = last_name.lower() + ' ' + first_name.lower() + ' ' + middle_name.lower()
  145. else:
  146. middle_name = '-'
  147. full_name = last_name.lower() + ' ' + first_name.lower()
  148. data = await state.get_data()
  149. data['first_name'] = first_name
  150. data['last_name'] = last_name
  151. data['middle_name'] = middle_name
  152. data['full_name'] = full_name
  153. await state.set_data(data)
  154. await state.set_state(AddPersonal.position)
  155. return await message.answer(f"ФИО записано.\n<b>Фамилия:</b><i> {last_name}</i>\n<b>Имя:</b><i> {first_name}</i>"
  156. f"\n<b>Отчество:</b><i> {middle_name}</i>\n\n"
  157. f"Шаг 2 из 5.\nОтправьте должность сотрудника.")
  158. @router.message(AddPersonal.position, RoleFilter(['admin']), BannedFilter(False))
  159. async def position_handler(message: types.Message, state: FSMContext):
  160. position = message.text.strip()
  161. data = await state.get_data()
  162. data['position'] = position
  163. await state.set_data(data)
  164. await state.set_state(AddPersonal.project)
  165. return await message.answer(f"Должность записана (<b>{position}</b>).\n\n"
  166. f"Шаг 3 из 5.\nОтправьте название проекта сотрудника.")
  167. @router.message(AddPersonal.project, RoleFilter(['admin']), BannedFilter(False))
  168. async def project_handler(message: types.Message, state: FSMContext):
  169. project = message.text.strip()
  170. data = await state.get_data()
  171. data['project'] = project
  172. await state.set_data(data)
  173. await state.set_state(AddPersonal.avatar)
  174. return await message.answer(f"Проект записан (<b>{project}</b>).\n\nШаг 4 из 5.\n"
  175. f"Отправьте фото аватарки сотрудника или введите /skip чтобы пропустить этот этап.")
  176. @router.message(AddPersonal.avatar, F.content_type == 'photo', RoleFilter(['admin']), BannedFilter(False))
  177. async def avatar_handler(message: types.Message, state: FSMContext):
  178. photo_id = message.photo[-1].file_id
  179. path = f'data/img/personal/{photo_id}.jpg'
  180. await bot.download(photo_id, path)
  181. data = await state.get_data()
  182. data['avatar'] = path
  183. await state.set_data(data)
  184. await state.set_state(AddPersonal.join_time)
  185. return await message.reply(f"Аватарка сохранена.\n\nШаг 5 из 5.\nОтправьте дату прихода сотрудника в формате "
  186. f"дд.мм.гггг или нажмите <b>«Сегодня»</b> для указания сегодняшнего дня.",
  187. reply_markup=kb.today_kb)
  188. @router.message(AddPersonal.avatar, RoleFilter(['admin']), BannedFilter(False))
  189. async def avatar_other_handler(message: types.Message, state: FSMContext):
  190. if message.text.strip() != '/skip':
  191. return await message.answer("Необходимо отправить аватарку как фото. (отправка файлом не принимается)")
  192. else:
  193. data = await state.get_data()
  194. data['avatar'] = 'Null'
  195. await state.set_data(data)
  196. await state.set_state(AddPersonal.join_time)
  197. return await message.reply(f"Добавление аватарки пропущено."
  198. f"\n\nШаг 5 из 5.\nОтправьте дату прихода сотрудника в формате "
  199. f"дд.мм.гггг или нажмите <b>«Сегодня»</b> для указания сегодняшнего дня.",
  200. reply_markup=kb.today_kb)
  201. @router.callback_query(AddPersonal.join_time, RoleFilter(['admin']), BannedFilter(False))
  202. async def today_handler(query: types.CallbackQuery, state: FSMContext):
  203. data = await state.get_data()
  204. role = await get_role(query.from_user.id)
  205. if query.data == "today":
  206. data['time_join'] = time.time()
  207. await state.set_data(data)
  208. await query.message.edit_reply_markup(None)
  209. await query.message.answer("Дата прихода установлена на сегодня."
  210. "\n\nНачинаю добавление нового сотрудника в базу...")
  211. return await show_personal(data, query.message, role, edit=False)
  212. if query.data == "confirm":
  213. await db.add_personal(data['first_name'], data['last_name'], data['middle_name'], data['full_name'],
  214. data['position'], data['project'], data['time_join'], data['avatar'])
  215. await query.message.edit_reply_markup(None)
  216. await query.answer("Новый сотрудник добавлен в базу.", show_alert=True)
  217. return await open_menu(query.message, state, edit=False)
  218. if query.data == "cancel":
  219. await query.message.edit_reply_markup(None)
  220. await query.answer("Добавление сотрудника отменено", show_alert=True)
  221. return await open_menu(query.message, state, edit=False)
  222. @router.message(AddPersonal.join_time, RoleFilter(['admin']), BannedFilter(False))
  223. async def date_handler(message: types.Message, state: FSMContext):
  224. role = await get_role(message.from_user.id)
  225. dt = message.text.strip().split('.')
  226. if len(dt) != 3 or len(dt[-1]) != 4:
  227. return await message.answer("Неверный формат даты. Формат: дд.мм.гггг\n* Пример: <i>28.09.2023</i>")
  228. try:
  229. date = datetime.datetime(int(dt[2]), int(dt[1]), int(dt[0]))
  230. timestamp = date.timestamp()
  231. except Exception as ex:
  232. return await message.answer(f"Неверный формат даты ({ex})")
  233. data = await state.get_data()
  234. data["time_join"] = timestamp
  235. await state.set_data(data)
  236. await message.reply("Дата прихода установлена.\n\nНачинаю добавление нового сотрудника в базу...")
  237. await show_personal(data, message, role, edit=False)
  238. # === Редактирование сотрудника ===
  239. # класс StatesGroup
  240. class EditPersonal(StatesGroup):
  241. choose = State()
  242. full_name = State()
  243. position = State()
  244. project = State()
  245. avatar = State()
  246. join_time = State()
  247. # хендлеры для этапов
  248. @router.message(EditPersonal.full_name, RoleFilter(['admin']), BannedFilter(False))
  249. async def name_handler(message: types.Message, state: FSMContext):
  250. name_parts = message.text.strip().split()
  251. if len(name_parts) > 3 or len(name_parts) < 2:
  252. return await message.answer("Необходимо ввести ФИО через пробел (Отчество при наличии).")
  253. last_name, first_name = name_parts[0], name_parts[1]
  254. if len(name_parts) == 3:
  255. middle_name = name_parts[2]
  256. full_name = last_name.lower() + ' ' + first_name.lower() + ' ' + middle_name.lower()
  257. else:
  258. middle_name = '-'
  259. full_name = last_name.lower() + ' ' + first_name.lower()
  260. data = await state.get_data()
  261. data['first_name'] = first_name
  262. data['last_name'] = last_name
  263. data['middle_name'] = middle_name
  264. data['full_name'] = full_name
  265. await state.set_data(data)
  266. await state.set_state(AddPersonal.position)
  267. await message.answer(f"ФИО изменено.\n<b>Фамилия:</b><i> {last_name}</i>\n<b>Имя:</b><i> {first_name}</i>"
  268. f"\n<b>Отчество:</b><i> {middle_name}</i>")
  269. await state.set_state(EditPersonal.choose)
  270. return await message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  271. @router.message(EditPersonal.position, RoleFilter(['admin']), BannedFilter(False))
  272. async def position_handler(message: types.Message, state: FSMContext):
  273. position = message.text.strip()
  274. data = await state.get_data()
  275. data['position'] = position
  276. await state.set_data(data)
  277. await state.set_state(AddPersonal.project)
  278. await message.answer(f"Должность изменена (<b>{position}</b>).")
  279. await state.set_state(EditPersonal.choose)
  280. return await message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  281. @router.message(EditPersonal.project, RoleFilter(['admin']), BannedFilter(False))
  282. async def project_handler(message: types.Message, state: FSMContext):
  283. project = message.text.strip()
  284. data = await state.get_data()
  285. data['project'] = project
  286. await state.set_data(data)
  287. await state.set_state(AddPersonal.avatar)
  288. await message.answer(f"Проект изменен (<b>{project}</b>).")
  289. await state.set_state(EditPersonal.choose)
  290. return await message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  291. @router.message(EditPersonal.avatar, F.content_type == 'photo', RoleFilter(['admin']), BannedFilter(False))
  292. async def avatar_handler(message: types.Message, state: FSMContext):
  293. photo_id = message.photo[-1].file_id
  294. path = f'data/img/personal/{photo_id}.jpg'
  295. await bot.download(photo_id, path)
  296. data = await state.get_data()
  297. data['avatar'] = path
  298. await state.set_data(data)
  299. await state.set_state(AddPersonal.join_time)
  300. await message.reply(f"Аватарка изменена.")
  301. await state.set_state(EditPersonal.choose)
  302. return await message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  303. @router.message(EditPersonal.avatar, RoleFilter(['admin']), BannedFilter(False))
  304. async def avatar_other_handler(message: types.Message, state: FSMContext):
  305. if message.text.strip() != '/del':
  306. return await message.answer("Необходимо отправить аватарку как фото. (отправка файлом не принимается)")
  307. else:
  308. data = await state.get_data()
  309. data['avatar'] = 'Null'
  310. await state.set_data(data)
  311. await state.set_state(AddPersonal.join_time)
  312. await message.reply(f"Аватар удален.")
  313. await state.set_state(EditPersonal.choose)
  314. return await message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  315. @router.message(EditPersonal.join_time, RoleFilter(['admin']), BannedFilter(False))
  316. async def date_handler(message: types.Message, state: FSMContext):
  317. dt = message.text.strip().split('.')
  318. if len(dt) != 3 or len(dt[-1]) != 4:
  319. return await message.answer("Неверный формат даты. Формат: дд.мм.гггг\n* Пример: <i>28.09.2023</i>")
  320. try:
  321. date = datetime.datetime(int(dt[2]), int(dt[1]), int(dt[0]))
  322. timestamp = date.timestamp()
  323. except Exception as ex:
  324. return await message.answer(f"Неверный формат даты ({ex})")
  325. data = await state.get_data()
  326. data["time_join"] = timestamp
  327. await state.set_data(data)
  328. await message.reply("Дата прихода изменена.")
  329. await state.set_state(EditPersonal.choose)
  330. return await message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  331. async def confirm_edit(query: types.CallbackQuery, state: FSMContext, data: dict, personal: dict):
  332. txt = "<i><b>Измененные значения:</b></i>"
  333. params = {"full_name": "ФИО", "position": "должность", "project": "проект", "avatar": "аватарка",
  334. "time_join": "дата прихода"}
  335. for k, v in data.items():
  336. if k == 'personal_id':
  337. continue
  338. if k == 'time_join':
  339. txt += f"\n\n• Значение <b>{params[k]}</b> изменено:\n<i>" \
  340. f"{datetime.datetime.fromtimestamp(personal[k]).strftime('%d.%m.%Y')}</i> -> <i>" \
  341. f"{datetime.datetime.fromtimestamp(data[k]).strftime('%d.%m.%Y')}</i>"
  342. elif k in params:
  343. txt += f"\n\n• Значение <b>{params[k]}</b> изменено:\n<i>{personal[k]}</i> -> <i>{data[k]}</i>"
  344. await db.change_personal(data['personal_id'], k, v)
  345. await query.answer("Информация о сотруднике изменена.")
  346. await query.message.edit_text(txt, reply_markup=None)
  347. return await open_menu(query.message, state, edit=False)
  348. # === Callback-хандлеры ===
  349. # callback редактирования
  350. @router.callback_query(EditPersonal.choose, RoleFilter(['admin']), BannedFilter(False))
  351. async def edit_callback(query: types.CallbackQuery, state: FSMContext):
  352. act = query.data.strip()
  353. data = await state.get_data()
  354. personal = await db.get_personal_by_id(data['personal_id'])
  355. if not personal:
  356. await open_menu(query.message, state, edit=True)
  357. return await query.answer("Указанный сотрудник не найден в базе бота")
  358. if act == "cancel":
  359. await query.message.edit_reply_markup(None)
  360. await query.answer("Редактирование сотрудника отменено", show_alert=True)
  361. return await open_menu(query.message, state, edit=False)
  362. if act == "cancel_b":
  363. await state.set_state(EditPersonal.choose)
  364. await query.message.edit_reply_markup(None)
  365. return await query.message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  366. if act == "confirm":
  367. return await confirm_edit(query, state, data, personal)
  368. if act == "name":
  369. await state.set_state(EditPersonal.full_name)
  370. return await query.message.edit_text("Введите новое ФИО.", reply_markup=kb.cancel_b_kb)
  371. if act == "position":
  372. await state.set_state(EditPersonal.position)
  373. return await query.message.edit_text("Введите новую должность.", reply_markup=kb.cancel_b_kb)
  374. if act == "project":
  375. await state.set_state(EditPersonal.project)
  376. return await query.message.edit_text("Введите новый проект.", reply_markup=kb.cancel_b_kb)
  377. if act == "avatar":
  378. await state.set_state(EditPersonal.avatar)
  379. return await query.message.edit_text("Отправьте фото новой аватарки пользователя.", reply_markup=kb.cancel_b_kb)
  380. if act == "time_join":
  381. await state.set_state(EditPersonal.join_time)
  382. return await query.message.edit_text("Введите новую дату прихода сотрудника в формате дд.мм.гггг", reply_markup=kb.cancel_b_kb)
  383. # callback карточки сотрудника
  384. async def pers_callback(query: types.CallbackQuery, state: FSMContext):
  385. act = query.data.split(':')[1]
  386. if act != 'back':
  387. data = query.data.split(':')[2]
  388. else:
  389. return await show_list(query.message, state)
  390. personal = await db.get_personal_by_id(data)
  391. role = await get_role(query.from_user.id)
  392. if not personal:
  393. return await query.answer("Выбранный сотрудник не найден в базе бота. (возможно он был удален)", show_alert=True)
  394. if act == "show":
  395. return await show_personal(personal, query.message, role, edit=True)
  396. if act == "del":
  397. if role != "admin":
  398. return await show_no_permission_message(query)
  399. await db.del_personal(personal['personal_id'])
  400. await query.answer("Сотрудник удален", show_alert=True)
  401. await query.message.edit_reply_markup(None)
  402. return await open_menu(query.message, state, edit=False)
  403. if act == "edit":
  404. if role != "admin":
  405. return await show_no_permission_message(query)
  406. await state.set_state(EditPersonal.choose)
  407. await state.set_data({'personal_id': personal['personal_id']})
  408. await query.message.edit_reply_markup(None)
  409. return await query.message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  410. # callback главного меню
  411. async def main_callback(query: types.CallbackQuery, state: FSMContext):
  412. act = query.data.split(':')[-1]
  413. role = await get_role(query.from_user.id)
  414. if act == "back":
  415. return await open_menu(query.message, state, edit=True)
  416. if act == "menu":
  417. await query.message.edit_reply_markup(None)
  418. return await open_menu(query.message, state, edit=False)
  419. if act == "list":
  420. return await show_list(query, state, edit=True)
  421. if act == "search":
  422. return await query.message.edit_text("<b><i>Меню поиска:</i></b>", reply_markup=kb.search_kb)
  423. if act == "add":
  424. if role != "admin":
  425. return await show_no_permission_message(query)
  426. await state.set_state(AddPersonal.full_name)
  427. await state.set_data({})
  428. return await query.message.answer(f"Вы перешли в режим добавления сотрудника, для выхода введите команду /menu."
  429. f"\n\nШаг 1 из 5.\nОтправьте боту ФИО сотрудника (Отчество при наличии)")
  430. # callback поиска
  431. async def search_callback(query: types.CallbackQuery, state: FSMContext):
  432. act = query.data.split(':')[1]
  433. if act == "name":
  434. text = f"Для поиска сотрудника по ФИО просто отправьте боту сообщение с текстом поискового запроса."
  435. return await query.answer(text, show_alert=True)
  436. if act == "project_user":
  437. text = f"Для поиска сотрудника по ФИО внутри проекта отправьте боту сообщение с текстом поискового " \
  438. f"запроса, добавив перед ФИО название проекта в квадратных скобках []."
  439. return await query.answer(text, show_alert=True)
  440. if act == "project":
  441. return await query.message.answer("<b>Выберите проект:</b>",
  442. reply_markup=await kb.create_project_search_kb(
  443. await db.get_projects_personal()))
  444. if act == "position":
  445. text = f"Для поиска сотрудника по ФИО внутри проекта отправьте боту сообщение с текстом поискового " \
  446. "запроса, добавив перед ФИО должность в фигурных скобках {}."
  447. return await query.answer(text, show_alert=True)
  448. if act == "position_list":
  449. return await query.message.answer("<b>Выберите должность:</b>",
  450. reply_markup=await kb.create_position_search_kb(
  451. await db.get_positions_personal()))
  452. if act == "time":
  453. text = f"Для поиска сотрудника по времени прихода отправьте боту промежуток времени в формате " \
  454. f"\n«(дд.мм.гггг - дд.мм.гггг)».\n* После скобок можно указать ФИО." \
  455. f"\n* конечная/начальная дата необязательна"
  456. return await query.answer(text, show_alert=True)
  457. if act == "s_project":
  458. project = query.data.split(':')[-1]
  459. personal = await db.get_personal(active=True, project=project)
  460. return await show_personal_list(personal, edit=False, message=query.message, search=True)
  461. if act == "s_pos":
  462. position = query.data.split(':')[-1]
  463. personal = await db.get_personal(active=True, position=position)
  464. return await show_personal_list(personal, edit=False, message=query.message, search=True)
  465. # основной callback хендлер
  466. @router.callback_query(BannedFilter(False))
  467. async def callback_handler(query: types.CallbackQuery, state: FSMContext):
  468. module = query.data.split(':')[0]
  469. if module == "main":
  470. return await main_callback(query, state)
  471. if module == "pers":
  472. return await pers_callback(query, state)
  473. if module == "cancel":
  474. return await open_menu(query.message, state, edit=True)
  475. if module == "cancel_b":
  476. await state.set_state(EditPersonal.choose)
  477. await query.message.edit_reply_markup(None)
  478. return await query.message.answer("Выберите что хотите сделать.", reply_markup=kb.edit_kb)
  479. if module == "search":
  480. return await search_callback(query, state)
  481. # === Поиск ===
  482. async def process_project_search(message: Message) -> tuple | Message:
  483. project = message.text.strip('[').split(']')[0].lower()
  484. projects = await db.get_projects_personal()
  485. if project not in [project_name.lower() for project_name in projects]:
  486. projects_txt = ''
  487. for proj in projects:
  488. projects_txt += f"\n • {proj}"
  489. return await message.answer(f"Проект <b>«{project}»</b> не найден.\n\nСписок проектов:{projects_txt}",
  490. reply_markup=kb.search_proj_kb)
  491. else:
  492. search = message.text.strip('[').split(']', 1)
  493. if len(search) < 2 or not search[-1].strip():
  494. return await message.answer("Запрос не должен быть пустым.")
  495. search_keys = message.text.strip().split(']', 1)[-1].split()
  496. return project, search_keys
  497. async def process_position_search(message: Message) -> tuple | Message:
  498. position = message.text.strip('{').split('}')[0].lower()
  499. positions = await db.get_positions_personal()
  500. if position not in [pos.lower() for pos in positions]:
  501. pos_txt = ''
  502. for proj in positions:
  503. pos_txt += f"\n • {proj}"
  504. return await message.answer(f"Проект <b>«{position}»</b> не найден.\n\nСписок проектов:{pos_txt}",
  505. reply_markup=kb.search_pos_kb)
  506. else:
  507. search = message.text.strip('{').split('}', 1)
  508. if len(search) < 2 or not search[-1].strip():
  509. return await message.answer("Запрос не должен быть пустым.")
  510. search_keys = message.text.strip().split('}', 1)[-1].split()
  511. return position, search_keys
  512. async def process_time_search(message: Message) -> tuple | Message:
  513. async def show_err_msg() -> Message:
  514. return await message.answer("Некорректный формат промежутка времени."
  515. "\n\nФормат: <b>«(дд.мм.гггг - дд.мм.гггг)»</b>."
  516. "\n\nПример: <i>(1.10.2022 - 28.9.2023)</i>"
  517. "\n\nили <i>(1.10.2022 - )</i> - поиск без конечной/начальной даты"
  518. "\n\nили <i>(1.10.2022 - 28.9.2023) Иванов</i> - поиск дата + ФИО")
  519. async def process_date(date_str: str) -> float | Message:
  520. if not date_str.strip():
  521. return await show_err_msg()
  522. date_str = date_str.strip().split('.')
  523. if len(date_str) < 3 or len(date_str[-1]) != 4:
  524. return await show_err_msg()
  525. try:
  526. datetime_ = datetime.datetime(int(date_str[2]), int(date_str[1]), int(date_str[0]))
  527. timestamp = datetime_.timestamp()
  528. except:
  529. return await show_err_msg()
  530. return timestamp
  531. time_data, search_data = message.text.strip('(').split(')')
  532. if len(time_data.split('-')) != 2:
  533. return await show_err_msg()
  534. start_time, end_time = time_data.split('-')
  535. # обработка начальной даты
  536. if start_time.strip():
  537. start_timestamp = await process_date(start_time)
  538. else:
  539. start_timestamp = 0
  540. # обработка конечной даты
  541. if end_time.strip():
  542. end_timestamp = await process_date(end_time)
  543. else:
  544. end_timestamp = time.time() + 24 * 60 * 60
  545. if not search_data.strip():
  546. search_keys = ['']
  547. else:
  548. search_keys = search_data.split()
  549. return start_timestamp, end_timestamp, search_keys
  550. @router.message(BannedFilter(False))
  551. async def search_handler(message: types.Message, state: FSMContext):
  552. # Обработка поиска по проекту
  553. if message.text.startswith('['):
  554. project, search_keys = await process_project_search(message)
  555. else:
  556. project = False
  557. search_keys = message.text.strip().split()
  558. # Обработка поиска по проекту
  559. if message.text.startswith('{'):
  560. position, search_keys = await process_position_search(message)
  561. else:
  562. position = False
  563. search_keys = message.text.strip().split()
  564. # Обработка поиска по времени
  565. message_text = message.text.split(']', 1)[-1]
  566. message_text = message.text.split('}', 1)[-1]
  567. if message_text.startswith('('):
  568. start_time, end_time, search_keys = await process_time_search(message)
  569. else:
  570. start_time = 0
  571. end_time = time.time() + 24 * 60 * 60
  572. if not message.text.strip():
  573. return await message.answer("Запрос не должен быть пустым.")
  574. finded = []
  575. try:
  576. search_keys = [' '.join(cmb) for cmb in list(permutations(search_keys + [''], 3))] + search_keys
  577. added = []
  578. for x in search_keys:
  579. if search_keys == ['']:
  580. find_results = await db.get_personal_by_time(active=True, project=project, position=position,
  581. time_search=(start_time, end_time))
  582. else:
  583. find_results = await db.get_personal_by_name(x.strip(), active=True, project=project, position=position,
  584. time_search=(start_time, end_time))
  585. for res in find_results:
  586. if res['personal_id'] not in added:
  587. added.append(res['personal_id'])
  588. finded.append(res)
  589. except Exception as ex:
  590. logger.warning(f"error while searching (uid: {message.from_user.id}, request: {message.text.strip()})", exc_info=True)
  591. text = f"<b><i>🔎 Результаты поиска:</i></b>"
  592. if not finded:
  593. text += f"\n\nНикого не найдено."
  594. return await message.reply(text, reply_markup=await kb.create_personal_list_kb(finded, search=True))