From 9b004c3a86dc5d045e639e1c171d409b06403942 Mon Sep 17 00:00:00 2001 From: kolo Date: Wed, 4 Mar 2026 01:21:06 +0300 Subject: [PATCH] update --- import_hours.py | 251 ++++++++++++++++++ import_residents.py | 102 +++++++ init_floors_rooms.sql | 35 --- parsed_residents.txt | 98 +++++++ parsed_rooms.txt | 44 +++ seed_rooms.py | 57 ++++ .../bot/admin_dialogs/residents_management.py | 60 ++++- 7 files changed, 598 insertions(+), 49 deletions(-) create mode 100644 import_hours.py create mode 100644 import_residents.py delete mode 100644 init_floors_rooms.sql create mode 100644 parsed_residents.txt create mode 100644 parsed_rooms.txt create mode 100644 seed_rooms.py diff --git a/import_hours.py b/import_hours.py new file mode 100644 index 0000000..87bbd94 --- /dev/null +++ b/import_hours.py @@ -0,0 +1,251 @@ +import argparse +import asyncio +import pandas as pd +import asyncpg +import re +import os + + +def clean_name(name: str) -> str: + if not isinstance(name, str): + return "" + name = re.sub(r'\s+[СC]$', '', name.strip()) + name = re.sub(r'\s*\d+\s*$', '', name) + return name.strip() + + +def parse_excel_raw(file_path: str): + excel_file = pd.ExcelFile(file_path) + resident_hours = {} + room_hours = {} + + for sheet_name in excel_file.sheet_names: + df = pd.read_excel(file_path, sheet_name=sheet_name) + + # --- Парсинг жильцов --- + if 'ФИО' in df.columns and 'Часы' in df.columns: + for _, row in df.iterrows(): + fio = clean_name(str(row['ФИО'])) + if not fio or str(fio).lower() == 'nan': + continue + try: + hours_raw = str(row['Часы']).split('(')[0].strip() + amount = int(float(hours_raw)) + except ValueError: + continue + + if fio in resident_hours: + resident_hours[fio] += amount + else: + resident_hours[fio] = amount + + # --- Парсинг комнат --- + elif df.iloc[0].astype(str).str.contains('Комната', case=False, na=False).any(): + header_idx = df[df.apply(lambda r: r.astype(str).str.contains('Комната', case=False).any(), axis=1)].index[0] + df.columns = df.iloc[header_idx] + df = df.iloc[header_idx + 1:].reset_index(drop=True) + + for _, row in df.iterrows(): + room_str = str(row['Комната']) + if not room_str or room_str.lower() == 'nan': + continue + + room_match = re.search(r'\b(\d{3,4})\b', room_str) + if not room_match: + continue + + room_number = int(room_match.group(1)) + + try: + punishment_raw = str(row['Наказание']).split('(')[0].replace('часов', '').replace(',', ';').strip() + amounts = [int(x.strip()) for x in re.findall(r'\d+', punishment_raw)] + amount = sum(amounts) if amounts else 0 + except (ValueError, AttributeError): + continue + + if amount <= 0: + continue + + if room_number in room_hours: + room_hours[room_number] += amount + else: + room_hours[room_number] = amount + + return resident_hours, room_hours + + +async def run_parse(database_url: str, file_path: str): + raw_resident_hours, raw_room_hours = parse_excel_raw(file_path) + + async with asyncpg.create_pool(database_url) as pool: + async with pool.acquire() as conn: + resident_rows = await conn.fetch("SELECT real_name FROM residents WHERE real_name IS NOT NULL;") + db_names = [row['real_name'] for row in resident_rows] + + room_rows = await conn.fetch("SELECT number FROM rooms;") + db_rooms = {row['number'] for row in room_rows} + + # --- Сопоставление жильцов --- + db_names_lower = {name.lower(): name for name in db_names} + matched_residents = {} + unmatched_residents = {} + + for raw_fio, amount in raw_resident_hours.items(): + fio_lower = raw_fio.lower() + matched_name = None + + if fio_lower in db_names_lower: + matched_name = db_names_lower[fio_lower] + else: + parts = fio_lower.split() + if len(parts) >= 2: + search_key = f"{parts[0]} {parts[1]}" + if search_key in db_names_lower: + matched_name = db_names_lower[search_key] + + if not matched_name: + for db_lower, db_orig in db_names_lower.items(): + if fio_lower in db_lower or db_lower in fio_lower: + matched_name = db_orig + break + + if matched_name: + matched_residents[matched_name] = matched_residents.get(matched_name, 0) + amount + else: + unmatched_residents[raw_fio] = unmatched_residents.get(raw_fio, 0) + amount + + # --- Сопоставление комнат --- + matched_rooms = {} + unmatched_rooms = {} + + for room_num, amount in raw_room_hours.items(): + if room_num in db_rooms: + matched_rooms[room_num] = matched_rooms.get(room_num, 0) + amount + else: + unmatched_rooms[room_num] = unmatched_rooms.get(room_num, 0) + amount + + # --- Запись в файлы --- + '''with open("parsed_residents.txt", "w", encoding="utf-8") as f: + for name, amount in sorted(matched_residents.items()): + f.write(f"{name} - {amount}\n")''' + + with open("parsed_rooms.txt", "w", encoding="utf-8") as f: + for room, amount in sorted(matched_rooms.items()): + f.write(f"{room} - {amount}\n") + + '''if unmatched_residents: + with open("unmatched_residents.txt", "w", encoding="utf-8") as f: + for name, amount in sorted(unmatched_residents.items()): + f.write(f"{name} - {amount}\n")''' + + if unmatched_rooms: + with open("unmatched_rooms.txt", "w", encoding="utf-8") as f: + for room, amount in sorted(unmatched_rooms.items()): + f.write(f"{room} - {amount}\n") + + +async def run_load(database_url: str, admin_id: int): + if not os.path.exists("parsed_residents.txt") or not os.path.exists("parsed_rooms.txt"): + raise FileNotFoundError("Файлы parsed_residents.txt или parsed_rooms.txt не найдены. Сначала запустите parse.") + + resident_transactions = [] + with open("parsed_residents.txt", "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split(" - ") + if len(parts) == 2: + resident_transactions.append((parts[0].strip(), int(parts[1].strip()))) + + room_transactions = [] + with open("parsed_rooms.txt", "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split(" - ") + if len(parts) == 2: + room_transactions.append((int(parts[0].strip()), int(parts[1].strip()))) + + async with asyncpg.create_pool(database_url) as pool: + async with pool.acquire() as conn: + # Маппинги + res_rows = await conn.fetch("SELECT id, real_name FROM residents WHERE real_name IS NOT NULL;") + name_to_id = {row['real_name']: row['id'] for row in res_rows} + + rm_rows = await conn.fetch("SELECT id, number FROM rooms;") + num_to_id = {row['number']: row['id'] for row in rm_rows} + + res_tx_params = [] + res_update_params = [] + for name, amount in resident_transactions: + res_id = name_to_id.get(name) + if res_id: + res_tx_params.append((res_id, "increase", amount, admin_id, "Инициализация существующих часов")) + res_update_params.append((amount, res_id)) + + rm_tx_params = [] + rm_update_params = [] + for room_num, amount in room_transactions: + rm_id = num_to_id.get(room_num) + if rm_id: + rm_tx_params.append((rm_id, "increase", amount, admin_id, "Инициализация существующих часов")) + rm_update_params.append((amount, rm_id)) + + async with conn.transaction(): + # Транзакции резидентов + if res_tx_params: + await conn.executemany( + """ + INSERT INTO hours_transactions + (resident_id, transaction_type, amount, admin_id, remark, created_at) + VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP); + """, res_tx_params + ) + # Обновление баланса резидентов + await conn.executemany( + """ + UPDATE residents + SET active_hours = active_hours + $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2; + """, res_update_params + ) + + # Транзакции комнат + if rm_tx_params: + await conn.executemany( + """ + INSERT INTO room_hours_transactions + (room_id, transaction_type, amount, admin_id, remark, created_at) + VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP); + """, rm_tx_params + ) + # Обновление баланса комнат + await conn.executemany( + """ + UPDATE rooms + SET active_hours = active_hours + $1 + WHERE id = $2; + """, rm_update_params + ) + + +async def main(): + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=["parse", "load"]) + parser.add_argument("--db-url", required=True) + parser.add_argument("--file", required=False) + parser.add_argument("--admin-id", type=int, default=2047958833) + args = parser.parse_args() + + if args.command == "parse": + if not args.file: + parser.error("Для команды parse требуется указать --file") + await run_parse(args.db_url, args.file) + elif args.command == "load": + await run_load(args.db_url, args.admin_id) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/import_residents.py b/import_residents.py new file mode 100644 index 0000000..2d3c74d --- /dev/null +++ b/import_residents.py @@ -0,0 +1,102 @@ +import argparse +import asyncio +import re +from docx import Document +import asyncpg + + +def parse_residents(docx_path: str) -> list[dict]: + doc = Document(docx_path) + results = [] + seen = set() + + for table in doc.tables: + current_room = None + for row in table.rows: + cells = row.cells + if len(cells) < 3: + continue + + cell0_text = cells[0].text.strip() + room_match = re.search(r'\b(\d{3,4})\b', cell0_text) + if room_match: + current_room = int(room_match.group(1)) + + name_text = cells[2].text.strip() + skip_keywords = ('Дата', 'Фамилия', 'Осталось', 'Совершеннолетних') + + if name_text and current_room and not any(kw in name_text for kw in skip_keywords): + clean_name = re.sub(r'\s+[СC]$', '', name_text).strip() + if clean_name and len(clean_name) > 2: + key = (current_room, clean_name) + if key not in seen: + seen.add(key) + results.append({"room_number": current_room, "real_name": clean_name}) + + return results + + +async def insert_data(database_url: str, residents: list[dict]) -> None: + async with asyncpg.create_pool(database_url) as pool: + async with pool.acquire() as conn: + async with conn.transaction(): + floor_rows = await conn.fetch("SELECT id, number FROM floors;") + floor_map = {row['number']: row['id'] for row in floor_rows} + + if not floor_map: + raise RuntimeError("Таблица floors пуста, невозможно привязать комнаты.") + + unique_rooms = set(r["room_number"] for r in residents) + room_insert_params = [] + + for room_num in unique_rooms: + floor_num = room_num // 100 + if floor_num not in floor_map: + raise ValueError(f"Для комнаты {room_num} не найден этаж {floor_num} в БД.") + room_insert_params.append((room_num, floor_map[floor_num])) + + await conn.executemany( + """ + INSERT INTO rooms (number, on_floor) + VALUES ($1, $2) + ON CONFLICT (number) DO NOTHING; + """, + room_insert_params + ) + + room_rows = await conn.fetch( + """ + SELECT id, number + FROM rooms + WHERE number = ANY($1::int[]); + """, + list(unique_rooms) + ) + room_map = {row['number']: row['id'] for row in room_rows} + + resident_params = [ + (r["real_name"], room_map[r["room_number"]]) + for r in residents + ] + + await conn.executemany( + """ + INSERT INTO residents (real_name, room, created_at, updated_at) + VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + """, + resident_params + ) + + +async def main() -> None: + parser = argparse.ArgumentParser(description="Импорт жильцов из docx в PostgreSQL") + parser.add_argument("--db-url", required=True, help="URL базы данных (напр. postgresql://user:pass@localhost/db)") + parser.add_argument("--file", required=True, help="Путь к docx файлу") + args = parser.parse_args() + + residents = parse_residents(args.file) + await insert_data(args.db_url, residents) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/init_floors_rooms.sql b/init_floors_rooms.sql deleted file mode 100644 index a781c05..0000000 --- a/init_floors_rooms.sql +++ /dev/null @@ -1,35 +0,0 @@ -DO $$ -DECLARE - first_names TEXT[] := ARRAY['Aleksandr', 'Dmitriy', 'Maksim', 'Ivan', 'Artem', 'Mikhail', 'Daniil', 'Egor', 'Andrey', 'Nikita', - 'Anna', 'Mariya', 'Elena', 'Olga', 'Ekaterina', 'Natalya', 'Tatyana', 'Irina', 'Yuliya', 'Svetlana']; - last_names TEXT[] := ARRAY['Ivanov', 'Petrov', 'Sidorov', 'Smirnov', 'Kuznetsov', 'Popov', 'Vasilev', 'Sokolov', 'Mikhaylov', 'Novikov', - 'Fedorov', 'Morozov', 'Volkov', 'Alekseev', 'Lebedev', 'Semenov', 'Egorov', 'Pavlov', 'Kozlov', 'Stepanov']; - floor_num INT; - floor_id INT; - room_num INT; - room_id INT; - residents_count INT; - i INT; - first_name TEXT; - last_name TEXT; -BEGIN - FOR floor_num IN 2..5 LOOP - INSERT INTO floors (number) VALUES (floor_num) RETURNING id INTO floor_id; - - FOR room_num IN 1..15 LOOP - INSERT INTO rooms (number, on_floor) - VALUES (floor_num * 100 + room_num, floor_id) - RETURNING id INTO room_id; - - residents_count := 2 + floor(random() * 2)::INT; - - FOR i IN 1..residents_count LOOP - first_name := first_names[1 + floor(random() * array_length(first_names, 1))::INT]; - last_name := last_names[1 + floor(random() * array_length(last_names, 1))::INT]; - - INSERT INTO residents (real_name, room, is_busy, active_hours, inactive_hours, created_at, updated_at) - VALUES (first_name || ' ' || last_name, room_id, false, 0, 0, NOW(), NOW()); - END LOOP; - END LOOP; - END LOOP; -END $$; diff --git a/parsed_residents.txt b/parsed_residents.txt new file mode 100644 index 0000000..3561c0d --- /dev/null +++ b/parsed_residents.txt @@ -0,0 +1,98 @@ +Абакунчик Анастасия - 55 +Адамович Анастасия - 30 +Алиева Ангелина - 5 +Анищенок Алена - 5 +Апацкая Александра - 25 +Басаревский Олег - 50 +Бобкова Софья - 60 +Бойко Виктория - 55 +Буйвол Екатерина - 45 +Вишневская Вирсавия - 20 +Володько Виктория - 10 +Гвоздович Иван - 45 +Гинцева Ева - 5 +Гумовский Тимур - 20 +Гутырчик Арина - 5 +Гутырчик Никита - 5 +Давлетшина Алеся - 35 +Доморацкий Ярослав - 105 +Дубинец Ксения - 20 +Дудинская Полина - 15 +Демова Маргарита - 10 +Дауд Фэрдос - 25 +Жилевич Кира - 15 +Зимницкая Валерия - 10 +Игнатенко Маргарита - 10 +Илюкевич Анна - 10 +Казак Милана - 10 +Канорин Егор - 75 +Каранько Дарья - 30 +Климбовский Иван - 55 +Костюкевич Лиза - 5 +Клименко Дарья - 15 +Климович Дмитрий - 30 +Ковалева Анна - 45 +Крукович Маргарита - 10 +Кулага Софья - 65 +Левизанова Алина - 10 +Лемеза Надежда - 55 +Липницкая Ксения - 45 +Лисица Наталья - 45 +Лобановская Кристина - 10 +Логинова Мария - 10 +Лось Артем - 85 +Лукьянов Павел - 15 +Ляхнович Яна - 20 +Максимова Юлия - 60 +Мальков Артем - 35 +Мамонько Анна - 80 +Мананкова Дарья - 90 +Мануйлов Артем - 65 +Мысливец Екатерина - 5 +Марьянский Дмитрий - 10 +Мельник Кира - 5 +Маковский Даниил - 35 +Мирошниченко Максим - 5 +Михаленок Полина - 15 +Мусаева Дильшад - 50 +Нагорная Марина - 10 +Неверовская Майя - 90 +Негода Никита - 40 +Немченко Виктория - 10 +Новикова Ксения - 80 +Островская Валерия - 10 +Павлусенко Богдан - 10 +Понамарева Алина - 10 +Пашкевич Николай - 60 +Пожиган Анастасия - 20 +Пусторжевцева Виктория - 10 +Рабцевич Дмитрий - 10 +Саланович Анастасия - 15 +Сергей Ангелина - 15 +Сечко Алина - 15 +Сивакова Татьяна - 5 +Скринник Маргарита - 35 +Сманцер Алесь - 75 +Скурьят Юлиана - 5 +Семенов Даниил - 5 +Спиченок Кира - 15 +Салоха Полина - 5 +Сушкевич Артем - 30 +Сырокваш Валерия - 5 +Терехова Арина - 15 +Тозик Константин - 5 +Тризнюк Станислав - 15 +Трухан Яна - 35 +Турончик Ангелина - 35 +Тюнис Надежда - 15 +Хацкевич Дарья - 5 +Цыбулько Екатерина - 10 +Черник Дарья - 5 +Чигир Дарья - 220 +Шарапова Ксения - 20 +Шидловский Георгий - 10 +Шишкина София - 25 +Щелоков Тимофей - 25 +Юхович Дарья - 105 +Янущик Владислав - 55 +Зимницкая Эвелина - 5 diff --git a/parsed_rooms.txt b/parsed_rooms.txt new file mode 100644 index 0000000..90c17d3 --- /dev/null +++ b/parsed_rooms.txt @@ -0,0 +1,44 @@ +201 - 5 +202 - 20 +203 - 5 +206 - 5 +207 - 10 +208 - 5 +209 - 5 +210 - 15 +211 - 10 +212 - 10 +213 - 5 +214 - 10 +215 - 55 +216 - 10 +301 - 5 +302 - 10 +303 - 35 +304 - 25 +305 - 15 +308 - 10 +310 - 15 +311 - 10 +313 - 5 +314 - 5 +315 - 20 +402 - 5 +403 - 5 +404 - 5 +405 - 10 +408 - 10 +412 - 10 +415 - 10 +501 - 10 +506 - 40 +507 - 20 +508 - 5 +509 - 20 +510 - 50 +511 - 100 +512 - 5 +513 - 15 +514 - 45 +515 - 15 +517 - 15 diff --git a/seed_rooms.py b/seed_rooms.py new file mode 100644 index 0000000..2268362 --- /dev/null +++ b/seed_rooms.py @@ -0,0 +1,57 @@ +import argparse +import asyncio +import asyncpg + + +async def seed_floors_and_rooms(database_url: str): + async with asyncpg.create_pool(database_url) as pool: + async with pool.acquire() as conn: + async with conn.transaction(): + + # Создаем этажи (со 2-го по 5-й) + floors = [2, 3, 4, 5] + await conn.executemany( + """ + INSERT INTO floors (number) + VALUES ($1) + ON CONFLICT (number) DO NOTHING; + """, + [(f,) for f in floors] + ) + print(f"Добавлены или уже существуют этажи: {floors}") + + # Получаем ID созданных этажей + floor_rows = await conn.fetch("SELECT id, number FROM floors WHERE number = ANY($1::int[]);", floors) + floor_map = {row['number']: row['id'] for row in floor_rows} + + # Создаем комнаты (x01 .. x16 для каждого этажа) + rooms_to_insert = [] + for floor_num in floors: + floor_id = floor_map[floor_num] + for room_suffix in range(1, 17): + room_number = floor_num * 100 + room_suffix # 201, 202, ..., 516 + rooms_to_insert.append((room_number, floor_id)) + + # Выполняем вставку комнат + await conn.executemany( + """ + INSERT INTO rooms (number, on_floor) + VALUES ($1, $2) + ON CONFLICT (number) DO NOTHING; + """, + rooms_to_insert + ) + print(f"Добавлены или уже существуют {len(rooms_to_insert)} комнат.") + + +async def main(): + parser = argparse.ArgumentParser(description="Инициализация этажей (2-5) и комнат (x01-x16)") + parser.add_argument("--db-url", required=True, help="URL базы данных") + args = parser.parse_args() + + await seed_floors_and_rooms(args.db_url) + print("Инициализация завершена.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/dutylog/application/bot/admin_dialogs/residents_management.py b/src/dutylog/application/bot/admin_dialogs/residents_management.py index e7d8b7d..8dbf773 100644 --- a/src/dutylog/application/bot/admin_dialogs/residents_management.py +++ b/src/dutylog/application/bot/admin_dialogs/residents_management.py @@ -27,6 +27,7 @@ async def get_residents_list_data( residents_repository: FromDishka[ResidentsRepository], rooms_repository: FromDishka[RoomsRepository], floors_repository: FromDishka[FloorsRepository], + users_repository: FromDishka[UsersRepository], **kwargs, ): all_residents = await residents_repository.get_all_residents() @@ -41,17 +42,25 @@ async def get_residents_list_data( else: floor_number = 999999 room_number = 999999 - residents_with_rooms.append((resident, floor_number, room_number)) + + is_admin = False + if resident.user_entity: + user = await users_repository.get_user_by_id(resident.user_entity) + if user and user.is_admin: + is_admin = True + + residents_with_rooms.append((resident, floor_number, room_number, is_admin)) residents_with_rooms.sort(key=lambda x: (x[1], x[2])) residents_data = [] - for resident, floor_number, room_number in residents_with_rooms: + for resident, floor_number, room_number, is_admin in residents_with_rooms: status = "🟢" if resident.is_busy else "⚪️" name = resident.real_name if resident.real_name else "Без имени" + admin_badge = " 👑" if is_admin else "" residents_data.append( - (f"{name} | Комната {room_number} | {status}", resident.id) + (f"{name} | Комната {room_number} | {status}{admin_badge}", resident.id) ) content = f""" @@ -79,12 +88,12 @@ async def get_resident_info_data( resident_id = dialog_manager.dialog_data.get("selected_resident_id") if not resident_id: - return {"info_content": "Ошибка: резидент не выбран", "is_busy": False, "from_search": False, "from_filter": False} + return {"info_content": "Ошибка: резидент не выбран", "is_busy": False, "is_admin": False, "from_search": False, "from_filter": False} resident = await residents_repository.get_resident_by_id(resident_id) if not resident: - return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "from_search": False, "from_filter": False} + return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "is_admin": False, "from_search": False, "from_filter": False} room = await rooms_repository.get_room_by_id(resident.room) room_number = room.number if room else "???" @@ -92,6 +101,7 @@ async def get_resident_info_data( name = resident.real_name if resident.real_name else "Без имени" status = "🟢 Занят" if resident.is_busy else "⚪️ Свободен" + is_admin = False user_info = "Не привязан" if resident.user_entity: user = await users_repository.get_user_by_id(resident.user_entity) @@ -101,19 +111,28 @@ async def get_resident_info_data( else: username = f"ID: {user.id}" user_info = f"{user.first_name} ({username})" + is_admin = user.is_admin + + admin_badge = " 👑" if is_admin else "" + + hours_info = "" + if not is_admin: + hours_info = f""" +🟢 Отработанные часы: {resident.inactive_hours} ч +🔴 Неотработанные часы: {resident.active_hours} ч +""" + else: + hours_info = "\n
👑 Это администратор
" info_content = f""" -
👤 Информация о резиденте
+
👤 Информация о резиденте{admin_badge}
ID: {resident.id} Имя: {name} Комната: {room_number} Статус: {status} Пользователь: {user_info} - -🟢 Отработанные часы: {resident.inactive_hours} ч -🔴 Неотработанные часы: {resident.active_hours} ч -""" +{hours_info}""" from_search = dialog_manager.dialog_data.get("from_search", False) from_filter = dialog_manager.dialog_data.get("from_filter", False) @@ -121,6 +140,7 @@ async def get_resident_info_data( return { "info_content": info_content, "is_busy": resident.is_busy, + "is_admin": is_admin, "from_search": from_search, "from_filter": from_filter, } @@ -478,17 +498,25 @@ async def get_search_results_data( else: floor_number = 999999 room_number = 999999 - residents_with_rooms.append((resident, floor_number, room_number)) + + is_admin = False + if resident.user_entity: + user = await users_repository.get_user_by_id(resident.user_entity) + if user and user.is_admin: + is_admin = True + + residents_with_rooms.append((resident, floor_number, room_number, is_admin)) residents_with_rooms.sort(key=lambda x: (x[1], x[2])) residents_data = [] - for resident, floor_number, room_number in residents_with_rooms: + for resident, floor_number, room_number, is_admin in residents_with_rooms: status = "🟢" if resident.is_busy else "⚪️" name = resident.real_name if resident.real_name else "Без имени" + admin_badge = " 👑" if is_admin else "" residents_data.append( - (f"{name} | Комната {room_number} | {status}", resident.id) + (f"{name} | Комната {room_number} | {status}{admin_badge}", resident.id) ) content = f""" @@ -565,28 +593,32 @@ resident_info_window = Window( Const("Добавить часы"), id="add_hours_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.add_hours_select), + when=~F["is_admin"], ), Button( Const("Отнять часы"), id="remove_hours_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.remove_hours_select), + when=~F["is_admin"], ), ), Button( Const("🔄 Перепривязать к комнате"), id="rebind_resident_btn", on_click=on_rebind_resident, + when=~F["is_admin"], ), Button( Const("🚪 Разлогинить"), id="logout_resident_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm), - when="is_busy", + when=F["is_busy"] & ~F["is_admin"], ), Button( Const("🗑 Удалить резидента"), id="delete_resident_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_delete_confirm), + when=~F["is_admin"], ), SwitchTo( Const("◀️ Назад к результатам поиска"),