"""Уведомления центра зоотехника при событиях синхронизации.""" from __future__ import annotations import logging from typing import Any, Dict, Iterable, List, Optional, Set from sqlalchemy import select from app import db from app.models import ( Ingredient, Recipe, SyncClient, SyncClientDisplayName, UnloadingGroup, ) from app.models.report import LoadingReport, UnloadingReport from app.models.sync import SyncQueue from app.services.notification_center_service import create_notification, format_detail_timestamp logger = logging.getLogger(__name__) RECIPE_DELIVERY_TABLES = frozenset( {"recipe", "ingredient", "unloading_group", "period_recipes"} ) REPORT_PARENT_TABLES = frozenset({"loading_report", "unloading_report"}) def resolve_sync_client_label(node_id: Optional[str]) -> str: if not node_id: return "терминал" display = db.session.execute( select(SyncClientDisplayName.display_name).where( SyncClientDisplayName.node_id == node_id ) ).scalar_one_or_none() if display and str(display).strip(): return str(display).strip() client_name = db.session.execute( select(SyncClient.client_name).where(SyncClient.node_id == node_id) ).scalar_one_or_none() if client_name and str(client_name).strip(): return str(client_name).strip() return f"терминал {node_id[:8]}" def _q(name: Optional[str]) -> str: text = (name or "").strip() return f"«{text}»" if text else "«без названия»" def _recipe_name(recipe_id: Optional[str]) -> str: if not recipe_id: return "без названия" recipe = db.session.get(Recipe, recipe_id) if recipe and not getattr(recipe, "is_deleted", False): return recipe.name or "без названия" return "без названия" def _recipe_id_from_sync_task(task: SyncQueue) -> Optional[str]: table_name = task.table_name record_id = task.record_id if table_name == "recipe": return record_id if table_name == "ingredient": row = db.session.get(Ingredient, record_id) return row.recipe_id if row else None if table_name == "unloading_group": row = db.session.get(UnloadingGroup, record_id) return row.recipe_id if row else None if table_name == "period_recipes": parts = str(record_id).split(":", 1) return parts[1] if len(parts) == 2 else None return None def notify_tasks_delivered_to_client( client_node_id: str, tasks: Iterable[SyncQueue] ) -> None: """Рейс (или его состав) доставлен на терминал клиента.""" terminal = resolve_sync_client_label(client_node_id) when = format_detail_timestamp() notified: Set[str] = set() for task in tasks: if task.table_name not in RECIPE_DELIVERY_TABLES: continue recipe_id = _recipe_id_from_sync_task(task) if not recipe_id or recipe_id in notified: continue notified.add(recipe_id) recipe_label = _q(_recipe_name(recipe_id)) try: create_notification( title="Рейс на терминале", detail=( f"Рейс {recipe_label} передан на терминал {terminal} — {when}" ), kind="info", category="sync", page="recipes", link_kind="recipe", link_id=recipe_id, ) except Exception: logger.exception( "[SYNC-NOTIFY] recipe delivered client=%s recipe=%s", client_node_id, recipe_id, ) def _report_notification_from_row( *, table_name: str, report_id: str, client_node_id: str, ) -> None: terminal = resolve_sync_client_label(client_node_id) when = format_detail_timestamp() if table_name == "loading_report": report = db.session.get(LoadingReport, report_id) if not report or getattr(report, "is_deleted", False): return recipe_label = _q(report.recipe_name or _recipe_name(report.recipe_id)) create_notification( title="Отчёт о загрузке", detail=( f"С терминала {terminal} пришёл отчёт о загрузке по рейсу {recipe_label} — {when}" ), kind="success", category="sync", page="reports", link_kind="report_loading", link_id=report_id, ) return if table_name == "unloading_report": report = db.session.get(UnloadingReport, report_id) if not report or getattr(report, "is_deleted", False): return recipe_label = _q(report.recipe_name or _recipe_name(report.recipe_id)) link_id = report.loading_report_id or report_id create_notification( title="Отчёт о выгрузке", detail=( f"С терминала {terminal} пришёл отчёт о выгрузке по рейсу {recipe_label} — {when}" ), kind="success", category="sync", page="reports", link_kind="report_loading", link_id=link_id, ) def notify_reports_pushed_from_client( client_node_id: str, applied_changes: Iterable[Dict[str, Any]] ) -> None: """Отчёт с полевого терминала принят на сервер.""" notified: Set[str] = set() for change in applied_changes: table_name = change.get("table_name") if table_name not in REPORT_PARENT_TABLES: continue action = change.get("action") if action not in ("create", "update"): continue record_id = change.get("record_id") if not record_id: continue key = f"{table_name}:{record_id}" if key in notified: continue notified.add(key) try: _report_notification_from_row( table_name=table_name, report_id=record_id, client_node_id=client_node_id, ) except Exception: logger.exception( "[SYNC-NOTIFY] report received client=%s table=%s id=%s", client_node_id, table_name, record_id, )