# ================================================================ # services/inventory/app/api/dashboard.py # ================================================================ """ Dashboard API endpoints for Inventory Service """ from datetime import datetime, timedelta from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Path, status from sqlalchemy.ext.asyncio import AsyncSession import structlog from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role, analytics_tier_required from shared.routing import RouteBuilder from app.core.database import get_db from app.services.inventory_service import InventoryService from app.services.food_safety_service import FoodSafetyService from app.services.dashboard_service import DashboardService from app.schemas.dashboard import ( InventoryDashboardSummary, FoodSafetyDashboard, BusinessModelInsights, InventoryAnalytics, DashboardFilter, AlertsFilter, StockStatusSummary, AlertSummary, RecentActivity ) from app.utils.cache import get_cached, set_cached, make_cache_key logger = structlog.get_logger() # Create route builder for consistent URL structure route_builder = RouteBuilder('inventory') router = APIRouter(tags=["dashboard"]) # ===== Dependency Injection ===== async def get_dashboard_service(db: AsyncSession = Depends(get_db)) -> DashboardService: """Get dashboard service with dependencies""" return DashboardService( inventory_service=InventoryService(), food_safety_service=FoodSafetyService() ) # ===== Main Dashboard Endpoints ===== @router.get( route_builder.build_dashboard_route("summary"), response_model=InventoryDashboardSummary ) async def get_inventory_dashboard_summary( tenant_id: UUID = Path(...), filters: Optional[DashboardFilter] = None, current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get comprehensive inventory dashboard summary with caching (30s TTL)""" try: # PHASE 2: Check cache first (only if no filters applied) cache_key = None if filters is None: cache_key = make_cache_key("inventory_dashboard", str(tenant_id)) cached_result = await get_cached(cache_key) if cached_result is not None: logger.debug("Cache hit for inventory dashboard", cache_key=cache_key, tenant_id=str(tenant_id)) return InventoryDashboardSummary(**cached_result) # Cache miss or filters applied - fetch from database summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters) # PHASE 2: Cache the result (30s TTL for inventory levels) if cache_key: await set_cached(cache_key, summary.model_dump(), ttl=30) logger.debug("Cached inventory dashboard", cache_key=cache_key, ttl=30, tenant_id=str(tenant_id)) logger.info("Dashboard summary retrieved", tenant_id=str(tenant_id), total_ingredients=summary.total_ingredients) return summary except Exception as e: logger.error("Error getting dashboard summary", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve dashboard summary" ) @router.get( route_builder.build_dashboard_route("overview") ) async def get_inventory_dashboard_overview( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """ Get lightweight inventory dashboard overview for health checks. This endpoint is optimized for frequent polling by the orchestrator service for dashboard health-status checks. It returns only essential metrics needed to determine inventory health status. """ try: overview = await dashboard_service.get_inventory_overview(db, tenant_id) logger.info("Inventory dashboard overview retrieved", tenant_id=str(tenant_id), out_of_stock_count=overview.get('out_of_stock_count', 0)) return overview except Exception as e: logger.error("Error getting inventory dashboard overview", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve inventory dashboard overview" ) @router.get( route_builder.build_dashboard_route("food-safety"), response_model=FoodSafetyDashboard ) async def get_food_safety_dashboard( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()), db: AsyncSession = Depends(get_db) ): """Get food safety dashboard data""" try: dashboard = await food_safety_service.get_food_safety_dashboard(db, tenant_id) logger.info("Food safety dashboard retrieved", tenant_id=str(tenant_id), compliance_percentage=dashboard.compliance_percentage) return dashboard except Exception as e: logger.error("Error getting food safety dashboard", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve food safety dashboard" ) @router.get( route_builder.build_dashboard_route("analytics"), response_model=InventoryAnalytics ) async def get_inventory_analytics( tenant_id: UUID = Path(...), days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get advanced inventory analytics""" try: analytics = await dashboard_service.get_inventory_analytics(db, tenant_id, days_back) logger.info("Inventory analytics retrieved", tenant_id=str(tenant_id), days_analyzed=days_back) return analytics except Exception as e: logger.error("Error getting inventory analytics", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve inventory analytics" ) @router.get( route_builder.build_dashboard_route("business-model"), response_model=BusinessModelInsights ) async def get_business_model_insights( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get business model insights based on inventory patterns""" try: insights = await dashboard_service.get_business_model_insights(db, tenant_id) logger.info("Business model insights retrieved", tenant_id=str(tenant_id), detected_model=insights.detected_model) return insights except Exception as e: logger.error("Error getting business model insights", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve business model insights" ) # ===== Detailed Dashboard Data Endpoints ===== @router.get( route_builder.build_dashboard_route("stock-status"), response_model=List[StockStatusSummary] ) async def get_stock_status_by_category( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get stock status breakdown by category""" try: stock_status = await dashboard_service.get_stock_status_by_category(db, tenant_id) return stock_status except Exception as e: logger.error("Error getting stock status by category", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve stock status by category" ) @router.get( route_builder.build_dashboard_route("alerts-summary"), response_model=List[AlertSummary] ) async def get_alerts_summary( tenant_id: UUID = Path(...), filters: Optional[AlertsFilter] = None, current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get alerts summary by type and severity""" try: alerts_summary = await dashboard_service.get_alerts_summary(db, tenant_id, filters) return alerts_summary except Exception as e: logger.error("Error getting alerts summary", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve alerts summary" ) @router.get( route_builder.build_dashboard_route("recent-activity"), response_model=List[RecentActivity] ) async def get_recent_activity( tenant_id: UUID = Path(...), limit: int = Query(20, ge=1, le=100, description="Number of activities to return"), activity_types: Optional[List[str]] = Query(None, description="Filter by activity types"), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get recent inventory activity""" try: activities = await dashboard_service.get_recent_activity( db, tenant_id, limit, activity_types ) return activities except Exception as e: logger.error("Error getting recent activity", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve recent activity" ) # ===== Real-time Data Endpoints ===== @router.get( route_builder.build_dashboard_route("live-metrics") ) async def get_live_metrics( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get real-time inventory metrics""" try: metrics = await dashboard_service.get_live_metrics(db, tenant_id) return { "timestamp": datetime.now().isoformat(), "metrics": metrics, "cache_ttl": 60 # Seconds } except Exception as e: logger.error("Error getting live metrics", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve live metrics" ) @router.get( route_builder.build_dashboard_route("temperature-status") ) async def get_temperature_monitoring_status( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), food_safety_service: FoodSafetyService = Depends(lambda: FoodSafetyService()), db: AsyncSession = Depends(get_db) ): """Get current temperature monitoring status""" try: temp_status = await food_safety_service.get_temperature_monitoring_status(db, tenant_id) return { "timestamp": datetime.now().isoformat(), "temperature_monitoring": temp_status } except Exception as e: logger.error("Error getting temperature status", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve temperature monitoring status" ) # ===== Dashboard Configuration Endpoints ===== @router.get( route_builder.build_dashboard_route("config") ) async def get_dashboard_config( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep) ): """Get dashboard configuration and settings""" try: from app.core.config import settings config = { "refresh_intervals": { "dashboard_cache_ttl": settings.DASHBOARD_CACHE_TTL, "alerts_refresh_interval": settings.ALERTS_REFRESH_INTERVAL, "temperature_log_interval": settings.TEMPERATURE_LOG_INTERVAL }, "features": { "food_safety_enabled": settings.FOOD_SAFETY_ENABLED, "temperature_monitoring_enabled": settings.TEMPERATURE_MONITORING_ENABLED, "business_model_detection": settings.ENABLE_BUSINESS_MODEL_DETECTION }, "thresholds": { "low_stock_default": settings.DEFAULT_LOW_STOCK_THRESHOLD, "reorder_point_default": settings.DEFAULT_REORDER_POINT, "expiration_warning_days": settings.EXPIRATION_WARNING_DAYS, "critical_expiration_hours": settings.CRITICAL_EXPIRATION_HOURS }, "business_model_thresholds": { "central_bakery_ingredients": settings.CENTRAL_BAKERY_THRESHOLD_INGREDIENTS, "individual_bakery_ingredients": settings.INDIVIDUAL_BAKERY_THRESHOLD_INGREDIENTS } } return config except Exception as e: logger.error("Error getting dashboard config", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve dashboard configuration" ) # ===== Export and Reporting Endpoints ===== @router.get( route_builder.build_operations_route("export/summary") ) async def export_dashboard_summary( tenant_id: UUID = Path(...), format: str = Query("json", description="Export format: json, csv, excel"), date_from: Optional[datetime] = Query(None, description="Start date for data export"), date_to: Optional[datetime] = Query(None, description="End date for data export"), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Export dashboard summary data""" try: if format.lower() not in ["json", "csv", "excel"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Unsupported export format. Use: json, csv, excel" ) export_data = await dashboard_service.export_dashboard_data( db, tenant_id, format, date_from, date_to ) logger.info("Dashboard data exported", tenant_id=str(tenant_id), format=format) return export_data except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error("Error exporting dashboard data", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to export dashboard data" ) # ===== Health and Status Endpoints ===== @router.get( route_builder.build_base_route("health") ) async def get_dashboard_health( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep) ): """Get dashboard service health status""" try: return { "service": "inventory-dashboard", "status": "healthy", "timestamp": datetime.now().isoformat(), "tenant_id": str(tenant_id), "features": { "food_safety": "enabled", "temperature_monitoring": "enabled", "business_model_detection": "enabled", "real_time_alerts": "enabled" } } except Exception as e: logger.error("Error getting dashboard health", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get dashboard health status" )