from datetime import datetime, timedelta from flask import Blueprint, jsonify, request from sqlalchemy import select from app import db from app.models import ( ComponentLoadingTime, LoadingReport, LoadingReportComponent, UnloadingReport, UnloadingReportGroup, ) from app.routes.auth_decorators import require_auth bp = Blueprint("reports", __name__, url_prefix="/api/reports") def _error(message: str, status_code: int = 400): return jsonify({"error": True, "message": message}), status_code def _parse_date_range(): date_from = (request.args.get("date_from") or "").strip() date_to = (request.args.get("date_to") or "").strip() if not date_from or not date_to: # Legacy fallback: last 24h when explicit range is missing. end = datetime.now() return end - timedelta(hours=24), end + timedelta(seconds=1), None try: start = datetime.strptime(date_from, "%Y-%m-%d") end = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1) return start, end, None except ValueError: return None, None, _error("Некорректный формат date_from/date_to (YYYY-MM-DD)", 400) @bp.get("/ping") def reports_ping(): """Временный health-check эндпоинт для модуля отчетов.""" return jsonify({"status": "ok"}), 200 @bp.get("/loading") @require_auth def list_loading_reports(): """Список отчётов загрузки с пагинацией.""" try: limit = int(request.args.get("limit", 100)) offset = int(request.args.get("offset", 0)) except ValueError: return _error("Некорректные параметры пагинации", 400) reports = db.session.execute( select(LoadingReport) .where(LoadingReport.is_deleted.is_(False)) .order_by(LoadingReport.start_time.desc()) .offset(offset) .limit(limit) ).scalars().all() return jsonify( [ { "id": r.id, "recipe_id": r.recipe_id, "recipe_name": r.recipe_name, "start_time": r.start_time.isoformat() if r.start_time else None, "end_time": r.end_time.isoformat() if r.end_time else None, "total_weight": r.total_weight, "version": r.version, } for r in reports ] ) @bp.get("") @require_auth def list_reports_legacy(): """Legacy-compatible aggregated reports endpoint used by reports UI.""" start, end, range_error = _parse_date_range() if range_error: return range_error try: limit = int(request.args.get("limit", 500)) offset = int(request.args.get("offset", 0)) except ValueError: return _error("Некорректные параметры пагинации", 400) reports = db.session.execute( select(LoadingReport) .where( LoadingReport.is_deleted.is_(False), LoadingReport.start_time >= start, LoadingReport.start_time < end, ) .order_by(LoadingReport.start_time.desc()) .offset(offset) .limit(limit) ).scalars().all() if not reports: return jsonify([]) report_ids = [r.id for r in reports] components = db.session.execute( select(LoadingReportComponent) .where( LoadingReportComponent.report_id.in_(report_ids), LoadingReportComponent.is_deleted.is_(False), ) .order_by(LoadingReportComponent.loading_order.asc()) ).scalars().all() loading_times = db.session.execute( select(ComponentLoadingTime) .where( ComponentLoadingTime.report_id.in_(report_ids), ComponentLoadingTime.is_deleted.is_(False), ) .order_by(ComponentLoadingTime.loading_order.asc()) ).scalars().all() unloading_rows = db.session.execute( select(UnloadingReport) .where( UnloadingReport.loading_report_id.in_(report_ids), UnloadingReport.is_deleted.is_(False), ) .order_by(UnloadingReport.start_time.desc()) ).scalars().all() unloading_by_loading_id = {u.loading_report_id: u for u in unloading_rows} unloading_ids = [u.id for u in unloading_rows] unloading_groups = ( db.session.execute( select(UnloadingReportGroup) .where( UnloadingReportGroup.report_id.in_(unloading_ids), UnloadingReportGroup.is_deleted.is_(False), ) .order_by(UnloadingReportGroup.order.asc()) ).scalars().all() if unloading_ids else [] ) components_map = {} for c in components: components_map.setdefault(c.report_id, []).append( { "id": c.id, "component_id": c.component_id, "component_name": c.component_name, "target_weight": c.target_weight, "actual_weight": c.actual_weight, "overload": c.overload, "loading_order": c.loading_order, } ) loading_times_map = {} for t in loading_times: loading_times_map.setdefault(t.report_id, []).append( { "id": t.id, "component_name": t.component_name, "start_time": t.start_time.isoformat() if t.start_time else None, "end_time": t.end_time.isoformat() if t.end_time else None, "loading_duration": t.loading_duration, "loading_order": t.loading_order, } ) unloading_groups_map = {} for g in unloading_groups: unloading_groups_map.setdefault(g.report_id, []).append( { "id": g.id, "name": g.name, "target_weight": g.target_weight, "unloaded_weight": g.unloaded_weight, "remaining_weight": g.remaining_weight, "distribution_type": g.distribution_type, "distribution_value": g.distribution_value, "order": g.order, } ) payload = [] for r in reports: unloading = unloading_by_loading_id.get(r.id) unloading_payload = None if unloading: unloading_payload = { "id": unloading.id, "start_time": unloading.start_time.isoformat() if unloading.start_time else None, "end_time": unloading.end_time.isoformat() if unloading.end_time else None, "total_weight": unloading.total_weight, "total_unloaded_weight": unloading.total_unloaded_weight, "remaining_weight": unloading.remaining_weight, "unloading_groups": unloading_groups_map.get(unloading.id, []), } payload.append( { "id": r.id, "recipe_id": r.recipe_id, "recipe_name": r.recipe_name, "start_time": r.start_time.isoformat() if r.start_time else None, "end_time": r.end_time.isoformat() if r.end_time else None, "target_mixing_time": r.target_mixing_time, "actual_mixing_time": r.actual_mixing_time, "total_weight": r.total_weight, "dispenser_type": r.dispenser_type, "components": components_map.get(r.id, []), "component_loading_times": loading_times_map.get(r.id, []), "unloading_data": unloading_payload, } ) return jsonify(payload) @bp.get("/unloading") @require_auth def list_unloading_reports(): """Список отчётов выгрузки с пагинацией.""" try: limit = int(request.args.get("limit", 100)) offset = int(request.args.get("offset", 0)) except ValueError: return _error("Некорректные параметры пагинации", 400) reports = db.session.execute( select(UnloadingReport) .where(UnloadingReport.is_deleted.is_(False)) .order_by(UnloadingReport.start_time.desc()) .offset(offset) .limit(limit) ).scalars().all() return jsonify( [ { "id": r.id, "recipe_id": r.recipe_id, "recipe_name": r.recipe_name, "start_time": r.start_time.isoformat() if r.start_time else None, "end_time": r.end_time.isoformat() if r.end_time else None, "total_weight": r.total_weight, "total_unloaded_weight": r.total_unloaded_weight, "remaining_weight": r.remaining_weight, "version": r.version, } for r in reports ] )