410 lines
15 KiB
Python
410 lines
15 KiB
Python
# ================================================================
|
|
# 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 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
|
|
)
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
router = APIRouter(prefix="/dashboard", 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("/tenants/{tenant_id}/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"""
|
|
try:
|
|
summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters)
|
|
|
|
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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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("/tenants/{tenant_id}/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"
|
|
) |