"""WESP-shaped reports API for copied static UI (/api/reports).""" from __future__ import annotations import json from datetime import UTC, datetime, timedelta from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query, status from app.core.database import session_scope from app.modules.sync.tenant import TenantContext, require_enterprise_zootech from app.modules.zootech.report_models import ZootechLoadingReport, ZootechUnloadingReport from sqlalchemy import select router = APIRouter() def _require_ent(enterprise_id: str, tenant: TenantContext) -> None: if tenant.enterprise_id != enterprise_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN") def _parse_payload(row: ZootechLoadingReport | ZootechUnloadingReport) -> dict[str, Any]: if not row.payload_json: return {} try: data = json.loads(row.payload_json) except json.JSONDecodeError: return {} return data if isinstance(data, dict) else {} def _parse_date_range( date_from: str | None, date_to: str | None ) -> tuple[datetime, datetime] | None: if not date_from or not date_to: end = datetime.now(UTC) return end - timedelta(hours=24), end + timedelta(seconds=1) try: start = datetime.strptime(date_from.strip(), "%Y-%m-%d").replace(tzinfo=UTC) end = datetime.strptime(date_to.strip(), "%Y-%m-%d").replace(tzinfo=UTC) + timedelta(days=1) return start, end except ValueError as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": True, "message": "Некорректный формат date_from/date_to (YYYY-MM-DD)"}, ) from exc def _parse_report_time(value: Any) -> datetime | None: if value is None: return None if isinstance(value, datetime): return value if value.tzinfo else value.replace(tzinfo=UTC) text = str(value).strip() if not text: return None try: parsed = datetime.fromisoformat(text.replace("Z", "+00:00")) except ValueError: return None return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC) def _in_range(start_time: datetime | None, window: tuple[datetime, datetime]) -> bool: if start_time is None: return False start, end = window return start <= start_time < end def _serialize_unloading_id(unloading_id: str, payload: dict[str, Any]) -> dict[str, Any]: groups = payload.get("unloading_groups") if not isinstance(groups, list): groups = payload.get("groups") if isinstance(payload.get("groups"), list) else [] return { "id": unloading_id, "start_time": payload.get("start_time"), "end_time": payload.get("end_time"), "total_weight": payload.get("total_weight"), "total_unloaded_weight": payload.get("total_unloaded_weight"), "remaining_weight": payload.get("remaining_weight"), "unloading_groups": groups, } def _serialize_unloading(row: ZootechUnloadingReport, payload: dict[str, Any]) -> dict[str, Any]: return _serialize_unloading_id(row.id, payload) @router.get("/reports") def list_reports_wesp( enterprise_id: str = Query(...), date_from: str | None = Query(None), date_to: str | None = Query(None), limit: int = Query(500, ge=1, le=2000), offset: int = Query(0, ge=0), tenant: TenantContext = Depends(require_enterprise_zootech), ): """Legacy-compatible aggregated loading reports for reports.html.""" _require_ent(enterprise_id, tenant) window = _parse_date_range(date_from, date_to) with session_scope() as db: loading_rows = list( db.scalars( select(ZootechLoadingReport).where( ZootechLoadingReport.enterprise_id == enterprise_id, ZootechLoadingReport.is_deleted.is_(False), ) ) ) unloading_rows = list( db.scalars( select(ZootechUnloadingReport).where( ZootechUnloadingReport.enterprise_id == enterprise_id, ZootechUnloadingReport.is_deleted.is_(False), ) ) ) loading_data = [(row.id, _parse_payload(row)) for row in loading_rows] unloading_data: list[tuple[str, str, dict[str, Any]]] = [] for row in unloading_rows: payload = _parse_payload(row) loading_id = str(payload.get("loading_report_id") or "").strip() if loading_id: unloading_data.append((loading_id, row.id, payload)) unloading_by_loading: dict[str, tuple[str, dict[str, Any]]] = { loading_id: (unloading_id, payload) for loading_id, unloading_id, payload in unloading_data } payload: list[dict[str, Any]] = [] for row_id, data in loading_data: start_time = _parse_report_time(data.get("start_time")) if window and not _in_range(start_time, window): continue unloading = unloading_by_loading.get(row_id) unloading_payload = ( _serialize_unloading_id(unloading[0], unloading[1]) if unloading else None ) payload.append( { "id": row_id, "recipe_id": data.get("recipe_id"), "recipe_name": data.get("recipe_name"), "start_time": data.get("start_time"), "end_time": data.get("end_time"), "target_mixing_time": data.get("target_mixing_time"), "actual_mixing_time": data.get("actual_mixing_time"), "total_weight": data.get("total_weight"), "dispenser_type": data.get("dispenser_type") or "dispenser", "components": data.get("components") if isinstance(data.get("components"), list) else [], "component_loading_times": ( data.get("component_loading_times") if isinstance(data.get("component_loading_times"), list) else [] ), "unloading_data": unloading_payload, } ) payload.sort(key=lambda item: str(item.get("start_time") or ""), reverse=True) result = payload[offset : offset + limit] return result @router.get("/reports/ping") def reports_ping_wesp(): return {"status": "ok"}