"""Запросы к таблицам intradesk_* (синхронизированные справочники).""" from __future__ import annotations import re import aiosqlite # ── Clients ─────────────────────────────────────────────────────────────────── async def upsert_client(db: aiosqlite.Connection, item: dict) -> None: await db.execute( """ INSERT INTO intradesk_clients (id, name, client_type, email, address, description, updated_at, synced_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(id) DO UPDATE SET name = excluded.name, client_type = excluded.client_type, email = excluded.email, address = excluded.address, description = excluded.description, updated_at = excluded.updated_at, synced_at = excluded.synced_at """, ( item["id"], item.get("name") or "", item.get("clientType"), item.get("email"), item.get("address"), item.get("description"), item.get("updatedAt"), ), ) async def get_all_clients(db: aiosqlite.Connection, search: str = "") -> list[dict]: q = "SELECT * FROM intradesk_clients" params: list = [] if search: q += " WHERE name LIKE ?" params.append(f"%{search}%") q += " ORDER BY name" async with db.execute(q, params) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def get_client(db: aiosqlite.Connection, client_id: int) -> dict | None: async with db.execute( "SELECT * FROM intradesk_clients WHERE id = ?", (client_id,) ) as cur: row = await cur.fetchone() return dict(row) if row else None # ── Users ───────────────────────────────────────────────────────────────────── async def upsert_user(db: aiosqlite.Connection, item: dict) -> None: # Основная группа — первая в списке groups client_id: int | None = None groups = item.get("groups") or [] if groups: client_id = groups[0].get("userGroupId") # Телефон — первый из списка phones = item.get("phoneNumbers") or [] phone = phones[0].get("phoneNumber") if phones else item.get("phoneNumber") await db.execute( """ INSERT INTO intradesk_users (id, name, first_name, last_name, middle_name, email, phone, client_id, is_archived, synced_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(id) DO UPDATE SET name = excluded.name, first_name = excluded.first_name, last_name = excluded.last_name, middle_name = excluded.middle_name, email = excluded.email, phone = excluded.phone, client_id = excluded.client_id, is_archived = excluded.is_archived, synced_at = excluded.synced_at """, ( item["id"], item.get("name") or "", item.get("firstName") or "", item.get("lastName") or "", item.get("middleName") or "", item.get("email") or "", phone, client_id, 1 if item.get("isArchived") else 0, ), ) async def get_users_for_client( db: aiosqlite.Connection, client_id: int ) -> list[dict]: async with db.execute( "SELECT * FROM intradesk_users WHERE client_id = ? ORDER BY name", (client_id,), ) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] # ── Tasks ───────────────────────────────────────────────────────────────────── async def upsert_task( db: aiosqlite.Connection, item: dict, dicts: dict[int, str] | None = None ) -> None: dicts = dicts or {} status_id = item.get("status") status_name = dicts.get(status_id, "") if status_id else "" priority_id = item.get("priority") priority_name = dicts.get(priority_id, "") if priority_id else "" executor_id = item.get("executor") executor_name = dicts.get(executor_id, "") if executor_id else "" initiator_id = item.get("initiator") initiator_name = dicts.get(initiator_id, "") if initiator_id else "" await db.execute( """ INSERT INTO intradesk_tasks ( id, task_number, name, description, status_id, status_name, priority_id, priority_name, client_id, initiator_id, initiator_name, executor_id, executor_name, executor_group, service_name, service_full_name, created_at, updated_at, closed_at, created_by, updated_by, synced_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')) ON CONFLICT(id) DO UPDATE SET task_number = excluded.task_number, name = excluded.name, description = excluded.description, status_id = excluded.status_id, status_name = excluded.status_name, priority_id = excluded.priority_id, priority_name = excluded.priority_name, client_id = excluded.client_id, initiator_id = excluded.initiator_id, initiator_name = excluded.initiator_name, executor_id = excluded.executor_id, executor_name = excluded.executor_name, executor_group = excluded.executor_group, service_name = excluded.service_name, service_full_name = excluded.service_full_name, created_at = excluded.created_at, updated_at = excluded.updated_at, closed_at = excluded.closed_at, created_by = excluded.created_by, updated_by = excluded.updated_by, synced_at = excluded.synced_at """, ( item["id"], item.get("tasknumber"), item.get("name") or "", item.get("description") or "", status_id, status_name, priority_id, priority_name, item.get("clientid"), initiator_id, initiator_name, executor_id, executor_name, item.get("executorgroup"), item.get("servicename") or "", item.get("servicefullname") or "", item.get("createdat"), item.get("updatedat"), item.get("closedat"), item.get("createdby") or "", item.get("updatedby") or "", ), ) async def get_tasks_for_client( db: aiosqlite.Connection, client_id: int, status_filter: str = "", page: int = 0, page_size: int = 30, ) -> list[dict]: q = "SELECT * FROM intradesk_tasks WHERE client_id = ?" params: list = [client_id] if status_filter: q += " AND status_name = ?" params.append(status_filter) q += " ORDER BY updated_at DESC LIMIT ? OFFSET ?" params += [page_size, page * page_size] async with db.execute(q, params) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def count_tasks_for_client( db: aiosqlite.Connection, client_id: int, status_filter: str = "" ) -> int: q = "SELECT COUNT(*) FROM intradesk_tasks WHERE client_id = ?" params: list = [client_id] if status_filter: q += " AND status_name = ?" params.append(status_filter) async with db.execute(q, params) as cur: row = await cur.fetchone() return row[0] if row else 0 async def get_distinct_statuses(db: aiosqlite.Connection) -> list[str]: async with db.execute( "SELECT DISTINCT status_name FROM intradesk_tasks WHERE status_name != '' ORDER BY status_name" ) as cur: rows = await cur.fetchall() return [r[0] for r in rows] # ── Sync log ────────────────────────────────────────────────────────────────── async def start_sync_log(db: aiosqlite.Connection, sync_type: str) -> int: async with db.execute( "INSERT INTO intradesk_sync_log (sync_type, status) VALUES (?, 'running')", (sync_type,), ) as cur: log_id = cur.lastrowid await db.commit() return log_id # type: ignore[return-value] async def finish_sync_log( db: aiosqlite.Connection, log_id: int, status: str, records_synced: int, error_message: str = "", ) -> None: await db.execute( """ UPDATE intradesk_sync_log SET status = ?, records_synced = ?, error_message = ?, finished_at = datetime('now') WHERE id = ? """, (status, records_synced, error_message or None, log_id), ) await db.commit() async def get_recent_sync_logs( db: aiosqlite.Connection, limit: int = 10 ) -> list[dict]: async with db.execute( "SELECT * FROM intradesk_sync_log ORDER BY id DESC LIMIT ?", (limit,) ) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def get_last_sync( db: aiosqlite.Connection, sync_type: str ) -> dict | None: async with db.execute( """ SELECT * FROM intradesk_sync_log WHERE sync_type = ? AND status = 'success' ORDER BY id DESC LIMIT 1 """, (sync_type,), ) as cur: row = await cur.fetchone() return dict(row) if row else None # ── Tasks by local org_id ───────────────────────────────────────────────────── _BOT_PAGE_SIZE = 8 async def get_tasks_for_local_org( db: aiosqlite.Connection, local_org_id: int, page: int = 0, page_size: int = _BOT_PAGE_SIZE, ) -> list[dict]: """ Вернуть intradesk_tasks для локальной организации (через organizations.intradesk_id). """ async with db.execute( """ SELECT it.* FROM intradesk_tasks it JOIN organizations o ON o.intradesk_id = it.client_id WHERE o.id = ? ORDER BY it.updated_at DESC LIMIT ? OFFSET ? """, (local_org_id, page_size, page * page_size), ) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def count_tasks_for_local_org( db: aiosqlite.Connection, local_org_id: int ) -> int: async with db.execute( """ SELECT COUNT(*) FROM intradesk_tasks it JOIN organizations o ON o.intradesk_id = it.client_id WHERE o.id = ? """, (local_org_id,), ) as cur: row = await cur.fetchone() return row[0] if row else 0 async def count_active_tasks_for_local_org( db: aiosqlite.Connection, local_org_id: int ) -> int: """Количество незакрытых заявок организации (closed_at IS NULL).""" async with db.execute( """ SELECT COUNT(*) FROM intradesk_tasks it JOIN organizations o ON o.intradesk_id = it.client_id WHERE o.id = ? AND it.closed_at IS NULL """, (local_org_id,), ) as cur: row = await cur.fetchone() return row[0] if row else 0 async def get_task_for_local_org( db: aiosqlite.Connection, task_id: int, local_org_id: int ) -> dict | None: """Получить одну задачу, убедившись что она принадлежит организации пользователя.""" async with db.execute( """ SELECT it.* FROM intradesk_tasks it JOIN organizations o ON o.intradesk_id = it.client_id WHERE it.id = ? AND o.id = ? """, (task_id, local_org_id), ) as cur: row = await cur.fetchone() return dict(row) if row else None # ── Contact person lookup ───────────────────────────────────────────────────── async def get_intradesk_contact_id_for_phone( db: aiosqlite.Connection, client_intradesk_id: int, phone: str ) -> int | None: """ Найти id контактного лица в Intradesk по клиенту + нормализованному телефону. phone — только цифры (как в bot_users.phone). """ async with db.execute( "SELECT id, phone FROM intradesk_users WHERE client_id = ?", (client_intradesk_id,), ) as cur: rows = await cur.fetchall() for row in rows: if _normalize_phone(row[1] or "") == phone: return row[0] return None # ── Whitelist sync ──────────────────────────────────────────────────────────── def _normalize_phone(raw: str) -> str: """Убираем всё кроме цифр, заменяем ведущую 8 → 7 (как в боте).""" digits = re.sub(r"\D", "", raw or "") if digits.startswith("8") and len(digits) == 11: digits = "7" + digits[1:] return digits async def sync_whitelist_from_intradesk(db: aiosqlite.Connection) -> int: """ Наполнить таблицы organizations и org_users данными из intradesk_clients / intradesk_users. Вызывается автоматически после синхронизации. Возвращает количество затронутых строк в org_users. """ # 1. Загружаем существующий маппинг intradesk_id → local org_id org_map: dict[int, int] = {} async with db.execute( "SELECT id, intradesk_id FROM organizations WHERE intradesk_id IS NOT NULL" ) as cur: for row in await cur.fetchall(): org_map[row[1]] = row[0] # 2. Для каждого intradesk_client создаём или обновляем organizations async with db.execute("SELECT id, name FROM intradesk_clients") as cur: clients = await cur.fetchall() for c in clients: iid, cname = c[0], c[1] if iid in org_map: # Обновляем имя на случай переименования в Intradesk await db.execute( "UPDATE organizations SET name = ? WHERE intradesk_id = ?", (cname, iid), ) else: # Создаём новую организацию async with db.execute( "INSERT INTO organizations (name, intradesk_id) VALUES (?, ?)", (cname, iid), ) as cur: org_map[iid] = cur.lastrowid # type: ignore[assignment] await db.commit() # 3. Для каждого активного intradesk_user с телефоном — upsert в org_users async with db.execute( """ SELECT id, phone, client_id, name, first_name, last_name FROM intradesk_users WHERE phone IS NOT NULL AND phone != '' AND is_archived = 0 """ ) as cur: users = await cur.fetchall() count = 0 for u in users: raw_phone = u[1] or "" phone = _normalize_phone(raw_phone) if len(phone) < 7: continue # явно некорректный номер client_id = u[2] org_id = org_map.get(client_id) if client_id else None if org_id is None: continue # клиент ещё не синхронизирован # Формируем отображаемое имя display = (u[3] or "").strip() or ( f"{u[5] or ''} {u[4] or ''}".strip() or None ) await db.execute( """ INSERT INTO org_users (org_id, phone, display_name) VALUES (?, ?, ?) ON CONFLICT(phone) DO UPDATE SET org_id = excluded.org_id, display_name = COALESCE(excluded.display_name, org_users.display_name) """, (org_id, phone, display), ) count += 1 await db.commit() return count