# ================================================================ # services/production/app/api/production.py # ================================================================ """ Production API endpoints """ from fastapi import APIRouter, Depends, HTTPException, Path, Query from typing import Optional, List from datetime import date, datetime, timedelta from uuid import UUID import structlog from shared.auth.decorators import get_current_user_dep from app.core.database import get_db from app.services.production_service import ProductionService from app.schemas.production import ( ProductionBatchCreate, ProductionBatchUpdate, ProductionBatchStatusUpdate, ProductionBatchResponse, ProductionBatchListResponse, ProductionScheduleCreate, ProductionScheduleUpdate, ProductionScheduleResponse, DailyProductionRequirements, ProductionDashboardSummary, ProductionMetrics, ProductionStatusEnum ) from app.core.config import settings logger = structlog.get_logger() router = APIRouter(tags=["production"]) def get_production_service() -> ProductionService: """Dependency injection for production service""" from app.core.database import database_manager return ProductionService(database_manager, settings) # ================================================================ # DASHBOARD ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/dashboard/summary", response_model=ProductionDashboardSummary) async def get_dashboard_summary( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Get production dashboard summary using shared auth""" try: summary = await production_service.get_dashboard_summary(tenant_id) logger.info("Retrieved production dashboard summary", tenant_id=str(tenant_id)) return summary except Exception as e: logger.error("Error getting daily production requirements", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get daily requirements") @router.get("/tenants/{tenant_id}/production/requirements", response_model=dict) async def get_production_requirements( tenant_id: UUID = Path(...), date: Optional[date] = Query(None, description="Target date for production requirements"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Get production requirements for procurement planning""" try: target_date = date or datetime.now().date() requirements = await production_service.get_production_requirements(tenant_id, target_date) logger.info("Retrieved production requirements for procurement", tenant_id=str(tenant_id), date=target_date.isoformat()) return requirements except Exception as e: logger.error("Error getting production requirements", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get production requirements") # ================================================================ # PRODUCTION BATCH ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/batches", response_model=ProductionBatchListResponse) async def list_production_batches( tenant_id: UUID = Path(...), status: Optional[ProductionStatusEnum] = Query(None, description="Filter by status"), product_id: Optional[UUID] = Query(None, description="Filter by product"), order_id: Optional[UUID] = Query(None, description="Filter by order"), start_date: Optional[date] = Query(None, description="Filter from date"), end_date: Optional[date] = Query(None, description="Filter to date"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(50, ge=1, le=100, description="Page size"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """List batches with filters: date, status, product, order_id""" try: filters = { "status": status, "product_id": str(product_id) if product_id else None, "order_id": str(order_id) if order_id else None, "start_date": start_date, "end_date": end_date } batch_list = await production_service.get_production_batches_list(tenant_id, filters, page, page_size) logger.info("Retrieved production batches list", tenant_id=str(tenant_id), filters=filters) return batch_list except Exception as e: logger.error("Error listing production batches", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to list production batches") @router.post("/tenants/{tenant_id}/production/batches", response_model=ProductionBatchResponse) async def create_production_batch( batch_data: ProductionBatchCreate, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Create a new production batch""" try: batch = await production_service.create_production_batch(tenant_id, batch_data) logger.info("Created production batch", batch_id=str(batch.id), tenant_id=str(tenant_id)) return ProductionBatchResponse.model_validate(batch) except ValueError as e: logger.warning("Invalid batch data", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error creating production batch", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to create production batch") @router.get("/tenants/{tenant_id}/production/batches/active", response_model=ProductionBatchListResponse) async def get_active_batches( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get currently active production batches""" try: from app.repositories.production_batch_repository import ProductionBatchRepository batch_repo = ProductionBatchRepository(db) batches = await batch_repo.get_active_batches(str(tenant_id)) batch_responses = [ProductionBatchResponse.model_validate(batch) for batch in batches] logger.info("Retrieved active production batches", count=len(batches), tenant_id=str(tenant_id)) return ProductionBatchListResponse( batches=batch_responses, total_count=len(batches), page=1, page_size=len(batches) ) except Exception as e: logger.error("Error getting active batches", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get active batches") @router.get("/tenants/{tenant_id}/production/batches/{batch_id}", response_model=ProductionBatchResponse) async def get_batch_details( tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get detailed information about a production batch""" try: from app.repositories.production_batch_repository import ProductionBatchRepository batch_repo = ProductionBatchRepository(db) batch = await batch_repo.get(batch_id) if not batch or str(batch.tenant_id) != str(tenant_id): raise HTTPException(status_code=404, detail="Production batch not found") logger.info("Retrieved production batch details", batch_id=str(batch_id), tenant_id=str(tenant_id)) return ProductionBatchResponse.model_validate(batch) except HTTPException: raise except Exception as e: logger.error("Error getting batch details", error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get batch details") @router.put("/tenants/{tenant_id}/production/batches/{batch_id}/status", response_model=ProductionBatchResponse) async def update_batch_status( status_update: ProductionBatchStatusUpdate, tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Update production batch status""" try: batch = await production_service.update_batch_status(tenant_id, batch_id, status_update) logger.info("Updated production batch status", batch_id=str(batch_id), new_status=status_update.status.value, tenant_id=str(tenant_id)) return ProductionBatchResponse.model_validate(batch) except ValueError as e: logger.warning("Invalid status update", error=str(e), batch_id=str(batch_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error updating batch status", error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to update batch status") @router.put("/tenants/{tenant_id}/production/batches/{batch_id}", response_model=ProductionBatchResponse) async def update_production_batch( batch_update: ProductionBatchUpdate, tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Update batch (e.g., start time, notes, status)""" try: batch = await production_service.update_production_batch(tenant_id, batch_id, batch_update) logger.info("Updated production batch", batch_id=str(batch_id), tenant_id=str(tenant_id)) return ProductionBatchResponse.model_validate(batch) except ValueError as e: logger.warning("Invalid batch update", error=str(e), batch_id=str(batch_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error updating production batch", error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to update production batch") @router.delete("/tenants/{tenant_id}/production/batches/{batch_id}") async def delete_production_batch( tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Cancel/delete draft batch (soft delete preferred)""" try: await production_service.delete_production_batch(tenant_id, batch_id) logger.info("Deleted production batch", batch_id=str(batch_id), tenant_id=str(tenant_id)) return {"message": "Production batch deleted successfully"} except ValueError as e: logger.warning("Cannot delete batch", error=str(e), batch_id=str(batch_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error deleting production batch", error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to delete production batch") @router.post("/tenants/{tenant_id}/production/batches/{batch_id}/start", response_model=ProductionBatchResponse) async def start_production_batch( tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Mark batch as started (updates actual_start_time)""" try: batch = await production_service.start_production_batch(tenant_id, batch_id) logger.info("Started production batch", batch_id=str(batch_id), tenant_id=str(tenant_id)) return ProductionBatchResponse.model_validate(batch) except ValueError as e: logger.warning("Cannot start batch", error=str(e), batch_id=str(batch_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error starting production batch", error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to start production batch") @router.post("/tenants/{tenant_id}/production/batches/{batch_id}/complete", response_model=ProductionBatchResponse) async def complete_production_batch( tenant_id: UUID = Path(...), batch_id: UUID = Path(...), completion_data: Optional[dict] = None, current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Complete batch — auto-calculates yield, duration, cost summary""" try: batch = await production_service.complete_production_batch(tenant_id, batch_id, completion_data) logger.info("Completed production batch", batch_id=str(batch_id), tenant_id=str(tenant_id)) return ProductionBatchResponse.model_validate(batch) except ValueError as e: logger.warning("Cannot complete batch", error=str(e), batch_id=str(batch_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error completing production batch", error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to complete production batch") @router.get("/tenants/{tenant_id}/production/batches/stats", response_model=dict) async def get_production_batch_stats( tenant_id: UUID = Path(...), start_date: Optional[date] = Query(None, description="Start date for stats"), end_date: Optional[date] = Query(None, description="End date for stats"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Aggregated stats: completed vs failed, avg yield, on-time rate""" try: # Default to last 30 days if no dates provided if not start_date: start_date = (datetime.now() - timedelta(days=30)).date() if not end_date: end_date = datetime.now().date() stats = await production_service.get_batch_statistics(tenant_id, start_date, end_date) logger.info("Retrieved production batch statistics", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat()) return stats except Exception as e: logger.error("Error getting production batch stats", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get production batch stats") # ================================================================ # PRODUCTION SCHEDULE ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/schedules", response_model=dict) async def get_production_schedule( tenant_id: UUID = Path(...), start_date: Optional[date] = Query(None, description="Start date for schedule"), end_date: Optional[date] = Query(None, description="End date for schedule"), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get production schedule for a date range""" try: # Default to next 7 days if no dates provided if not start_date: start_date = datetime.now().date() if not end_date: end_date = start_date + timedelta(days=7) from app.repositories.production_schedule_repository import ProductionScheduleRepository schedule_repo = ProductionScheduleRepository(db) schedules = await schedule_repo.get_schedules_by_date_range( str(tenant_id), start_date, end_date ) schedule_data = { "start_date": start_date.isoformat(), "end_date": end_date.isoformat(), "schedules": [ { "id": str(schedule.id), "date": schedule.schedule_date.isoformat(), "shift_start": schedule.shift_start.isoformat(), "shift_end": schedule.shift_end.isoformat(), "capacity_utilization": schedule.utilization_percentage, "batches_planned": schedule.total_batches_planned, "is_finalized": schedule.is_finalized } for schedule in schedules ], "total_schedules": len(schedules) } logger.info("Retrieved production schedule", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat(), schedules_count=len(schedules)) return schedule_data except Exception as e: logger.error("Error getting production schedule", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get production schedule") @router.get("/tenants/{tenant_id}/production/schedules/{schedule_id}", response_model=ProductionScheduleResponse) async def get_production_schedule_details( tenant_id: UUID = Path(...), schedule_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Retrieve full schedule details including assignments""" try: from app.repositories.production_schedule_repository import ProductionScheduleRepository schedule_repo = ProductionScheduleRepository(db) schedule = await schedule_repo.get(schedule_id) if not schedule or str(schedule.tenant_id) != str(tenant_id): raise HTTPException(status_code=404, detail="Production schedule not found") logger.info("Retrieved production schedule details", schedule_id=str(schedule_id), tenant_id=str(tenant_id)) return ProductionScheduleResponse.model_validate(schedule) except HTTPException: raise except Exception as e: logger.error("Error getting production schedule details", error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get production schedule details") @router.post("/tenants/{tenant_id}/production/schedules", response_model=ProductionScheduleResponse) async def create_production_schedule( schedule_data: ProductionScheduleCreate, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Generate or manually create a daily/shift schedule""" try: schedule = await production_service.create_production_schedule(tenant_id, schedule_data) logger.info("Created production schedule", schedule_id=str(schedule.id), tenant_id=str(tenant_id)) return ProductionScheduleResponse.model_validate(schedule) except ValueError as e: logger.warning("Invalid schedule data", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error creating production schedule", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to create production schedule") @router.put("/tenants/{tenant_id}/production/schedules/{schedule_id}", response_model=ProductionScheduleResponse) async def update_production_schedule( schedule_update: ProductionScheduleUpdate, tenant_id: UUID = Path(...), schedule_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Edit schedule before finalizing""" try: schedule = await production_service.update_production_schedule(tenant_id, schedule_id, schedule_update) logger.info("Updated production schedule", schedule_id=str(schedule_id), tenant_id=str(tenant_id)) return ProductionScheduleResponse.model_validate(schedule) except ValueError as e: logger.warning("Invalid schedule update", error=str(e), schedule_id=str(schedule_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error updating production schedule", error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to update production schedule") @router.post("/tenants/{tenant_id}/production/schedules/{schedule_id}/finalize", response_model=ProductionScheduleResponse) async def finalize_production_schedule( tenant_id: UUID = Path(...), schedule_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Lock schedule; prevents further changes""" try: schedule = await production_service.finalize_production_schedule(tenant_id, schedule_id) logger.info("Finalized production schedule", schedule_id=str(schedule_id), tenant_id=str(tenant_id)) return ProductionScheduleResponse.model_validate(schedule) except ValueError as e: logger.warning("Cannot finalize schedule", error=str(e), schedule_id=str(schedule_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error finalizing production schedule", error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to finalize production schedule") @router.get("/tenants/{tenant_id}/production/schedules/{date}/optimize", response_model=dict) async def optimize_production_schedule( tenant_id: UUID = Path(...), target_date: date = Path(..., alias="date"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Trigger AI-based rescheduling suggestion based on demand/capacity""" try: optimization_result = await production_service.optimize_schedule(tenant_id, target_date) logger.info("Generated schedule optimization suggestions", tenant_id=str(tenant_id), date=target_date.isoformat()) return optimization_result except Exception as e: logger.error("Error optimizing production schedule", error=str(e), tenant_id=str(tenant_id), date=target_date.isoformat()) raise HTTPException(status_code=500, detail="Failed to optimize production schedule") @router.get("/tenants/{tenant_id}/production/schedules/capacity-usage", response_model=dict) async def get_schedule_capacity_usage( tenant_id: UUID = Path(...), start_date: Optional[date] = Query(None, description="Start date for capacity usage"), end_date: Optional[date] = Query(None, description="End date for capacity usage"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """View capacity utilization over time (for reporting)""" try: # Default to last 30 days if no dates provided if not start_date: start_date = (datetime.now() - timedelta(days=30)).date() if not end_date: end_date = datetime.now().date() capacity_usage = await production_service.get_capacity_usage_report(tenant_id, start_date, end_date) logger.info("Retrieved schedule capacity usage", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat()) return capacity_usage except Exception as e: logger.error("Error getting schedule capacity usage", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get schedule capacity usage") # ================================================================ # CAPACITY MANAGEMENT ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/capacity/status", response_model=dict) async def get_capacity_status( tenant_id: UUID = Path(...), date: Optional[date] = Query(None, description="Date for capacity status"), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get production capacity status for a specific date""" try: target_date = date or datetime.now().date() from app.repositories.production_capacity_repository import ProductionCapacityRepository capacity_repo = ProductionCapacityRepository(db) capacity_summary = await capacity_repo.get_capacity_utilization_summary( str(tenant_id), target_date, target_date ) logger.info("Retrieved capacity status", tenant_id=str(tenant_id), date=target_date.isoformat()) return capacity_summary except Exception as e: logger.error("Error getting capacity status", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get capacity status") @router.get("/tenants/{tenant_id}/production/capacity", response_model=dict) async def list_production_capacity( tenant_id: UUID = Path(...), resource_type: Optional[str] = Query(None, description="Filter by resource type (equipment/staff)"), date: Optional[date] = Query(None, description="Filter by date"), availability: Optional[bool] = Query(None, description="Filter by availability"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(50, ge=1, le=100, description="Page size"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Filter by resource_type (equipment/staff), date, availability""" try: filters = { "resource_type": resource_type, "date": date, "availability": availability } capacity_list = await production_service.get_capacity_list(tenant_id, filters, page, page_size) logger.info("Retrieved production capacity list", tenant_id=str(tenant_id), filters=filters) return capacity_list except Exception as e: logger.error("Error listing production capacity", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to list production capacity") @router.get("/tenants/{tenant_id}/production/capacity/{resource_id}/availability", response_model=dict) async def check_resource_availability( tenant_id: UUID = Path(...), resource_id: str = Path(...), start_time: datetime = Query(..., description="Start time for availability check"), end_time: datetime = Query(..., description="End time for availability check"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Check if oven/station is free during a time window""" try: availability = await production_service.check_resource_availability( tenant_id, resource_id, start_time, end_time ) logger.info("Checked resource availability", tenant_id=str(tenant_id), resource_id=resource_id) return availability except Exception as e: logger.error("Error checking resource availability", error=str(e), tenant_id=str(tenant_id), resource_id=resource_id) raise HTTPException(status_code=500, detail="Failed to check resource availability") @router.post("/tenants/{tenant_id}/production/capacity/reserve", response_model=dict) async def reserve_capacity( reservation_data: dict, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Reserve equipment/staff for a future batch""" try: reservation = await production_service.reserve_capacity(tenant_id, reservation_data) logger.info("Reserved production capacity", tenant_id=str(tenant_id)) return reservation except ValueError as e: logger.warning("Invalid reservation data", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error reserving capacity", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to reserve capacity") @router.put("/tenants/{tenant_id}/production/capacity/{capacity_id}", response_model=dict) async def update_capacity( capacity_update: dict, tenant_id: UUID = Path(...), capacity_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Update maintenance status or efficiency rating""" try: updated_capacity = await production_service.update_capacity(tenant_id, capacity_id, capacity_update) logger.info("Updated production capacity", tenant_id=str(tenant_id), capacity_id=str(capacity_id)) return updated_capacity except ValueError as e: logger.warning("Invalid capacity update", error=str(e), capacity_id=str(capacity_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error updating capacity", error=str(e), tenant_id=str(tenant_id), capacity_id=str(capacity_id)) raise HTTPException(status_code=500, detail="Failed to update capacity") @router.get("/tenants/{tenant_id}/production/capacity/bottlenecks", response_model=dict) async def get_capacity_bottlenecks( tenant_id: UUID = Path(...), days_ahead: int = Query(3, ge=1, le=30, description="Number of days to predict ahead"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """AI-powered endpoint: returns predicted bottlenecks for next 3 days""" try: bottlenecks = await production_service.predict_capacity_bottlenecks(tenant_id, days_ahead) logger.info("Retrieved capacity bottleneck predictions", tenant_id=str(tenant_id), days_ahead=days_ahead) return bottlenecks except Exception as e: logger.error("Error getting capacity bottlenecks", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get capacity bottlenecks") # ================================================================ # QUALITY CHECK ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/quality-checks", response_model=dict) async def list_quality_checks( tenant_id: UUID = Path(...), batch_id: Optional[UUID] = Query(None, description="Filter by batch"), product_id: Optional[UUID] = Query(None, description="Filter by product"), start_date: Optional[date] = Query(None, description="Filter from date"), end_date: Optional[date] = Query(None, description="Filter to date"), pass_fail: Optional[bool] = Query(None, description="Filter by pass/fail"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(50, ge=1, le=100, description="Page size"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """List checks filtered by batch, product, date, pass/fail""" try: filters = { "batch_id": str(batch_id) if batch_id else None, "product_id": str(product_id) if product_id else None, "start_date": start_date, "end_date": end_date, "pass_fail": pass_fail } quality_checks = await production_service.get_quality_checks_list(tenant_id, filters, page, page_size) logger.info("Retrieved quality checks list", tenant_id=str(tenant_id), filters=filters) return quality_checks except Exception as e: logger.error("Error listing quality checks", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to list quality checks") @router.get("/tenants/{tenant_id}/production/batches/{batch_id}/quality-checks", response_model=dict) async def get_batch_quality_checks( tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Get all quality checks for a specific batch""" try: quality_checks = await production_service.get_batch_quality_checks(tenant_id, batch_id) logger.info("Retrieved quality checks for batch", tenant_id=str(tenant_id), batch_id=str(batch_id)) return quality_checks except Exception as e: logger.error("Error getting batch quality checks", error=str(e), tenant_id=str(tenant_id), batch_id=str(batch_id)) raise HTTPException(status_code=500, detail="Failed to get batch quality checks") @router.post("/tenants/{tenant_id}/production/quality-checks", response_model=dict) async def create_quality_check( quality_check_data: dict, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Submit a new quality inspection result""" try: quality_check = await production_service.create_quality_check(tenant_id, quality_check_data) logger.info("Created quality check", tenant_id=str(tenant_id)) return quality_check except ValueError as e: logger.warning("Invalid quality check data", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error creating quality check", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to create quality check") @router.get("/tenants/{tenant_id}/production/quality-checks/trends", response_model=dict) async def get_quality_trends( tenant_id: UUID = Path(...), start_date: Optional[date] = Query(None, description="Start date for trends"), end_date: Optional[date] = Query(None, description="End date for trends"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Returns defect trends, average scores by product/equipment""" try: # Default to last 30 days if no dates provided if not start_date: start_date = (datetime.now() - timedelta(days=30)).date() if not end_date: end_date = datetime.now().date() trends = await production_service.get_quality_trends(tenant_id, start_date, end_date) logger.info("Retrieved quality trends", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat()) return trends except Exception as e: logger.error("Error getting quality trends", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get quality trends") @router.get("/tenants/{tenant_id}/production/quality-checks/alerts", response_model=dict) async def get_quality_alerts( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Active alerts where corrective action is needed""" try: alerts = await production_service.get_quality_alerts(tenant_id) logger.info("Retrieved quality alerts", tenant_id=str(tenant_id)) return alerts except Exception as e: logger.error("Error getting quality alerts", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get quality alerts") @router.put("/tenants/{tenant_id}/production/quality-checks/{check_id}", response_model=dict) async def update_quality_check( check_update: dict, tenant_id: UUID = Path(...), check_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Add photos, notes, or mark corrective actions as completed""" try: updated_check = await production_service.update_quality_check(tenant_id, check_id, check_update) logger.info("Updated quality check", tenant_id=str(tenant_id), check_id=str(check_id)) return updated_check except ValueError as e: logger.warning("Invalid quality check update", error=str(e), check_id=str(check_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error updating quality check", error=str(e), tenant_id=str(tenant_id), check_id=str(check_id)) raise HTTPException(status_code=500, detail="Failed to update quality check") # ================================================================ # ANALYTICS / CROSS-CUTTING ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/analytics/performance", response_model=dict) async def get_performance_analytics( tenant_id: UUID = Path(...), start_date: Optional[date] = Query(None, description="Start date for analytics"), end_date: Optional[date] = Query(None, description="End date for analytics"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Daily performance: completion rate, waste %, labor cost per unit""" try: # Default to last 30 days if no dates provided if not start_date: start_date = (datetime.now() - timedelta(days=30)).date() if not end_date: end_date = datetime.now().date() performance = await production_service.get_performance_analytics(tenant_id, start_date, end_date) logger.info("Retrieved performance analytics", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat()) return performance except Exception as e: logger.error("Error getting performance analytics", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get performance analytics") @router.get("/tenants/{tenant_id}/production/analytics/yield-trends", response_model=dict) async def get_yield_trends_analytics( tenant_id: UUID = Path(...), period: str = Query("week", regex="^(week|month)$", description="Time period for trends"), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Yield trendline by product over past week/month""" try: yield_trends = await production_service.get_yield_trends_analytics(tenant_id, period) logger.info("Retrieved yield trends analytics", tenant_id=str(tenant_id), period=period) return yield_trends except Exception as e: logger.error("Error getting yield trends analytics", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get yield trends analytics") @router.get("/tenants/{tenant_id}/production/analytics/top-defects", response_model=dict) async def get_top_defects_analytics( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Top 5 defect types across batches""" try: top_defects = await production_service.get_top_defects_analytics(tenant_id) logger.info("Retrieved top defects analytics", tenant_id=str(tenant_id)) return top_defects except Exception as e: logger.error("Error getting top defects analytics", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get top defects analytics") @router.get("/tenants/{tenant_id}/production/analytics/equipment-efficiency", response_model=dict) async def get_equipment_efficiency_analytics( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Rank ovens/mixers by uptime, yield, downtime""" try: equipment_efficiency = await production_service.get_equipment_efficiency_analytics(tenant_id) logger.info("Retrieved equipment efficiency analytics", tenant_id=str(tenant_id)) return equipment_efficiency except Exception as e: logger.error("Error getting equipment efficiency analytics", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get equipment efficiency analytics") @router.post("/tenants/{tenant_id}/production/analytics/generate-report", response_model=dict) async def generate_analytics_report( report_config: dict, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Generate PDF report (daily summary, compliance audit)""" try: report = await production_service.generate_analytics_report(tenant_id, report_config) logger.info("Generated analytics report", tenant_id=str(tenant_id)) return report except ValueError as e: logger.warning("Invalid report configuration", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error generating analytics report", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to generate analytics report") # ================================================================ # METRICS AND ANALYTICS ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/metrics/yield", response_model=dict) async def get_yield_metrics( tenant_id: UUID = Path(...), start_date: date = Query(..., description="Start date for metrics"), end_date: date = Query(..., description="End date for metrics"), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get production yield metrics for analysis""" try: from app.repositories.production_batch_repository import ProductionBatchRepository batch_repo = ProductionBatchRepository(db) metrics = await batch_repo.get_production_metrics(str(tenant_id), start_date, end_date) logger.info("Retrieved yield metrics", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat()) return metrics except Exception as e: logger.error("Error getting yield metrics", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get yield metrics") # ================================================================ # QUALITY TEMPLATES ENDPOINTS # ================================================================ from app.repositories.quality_template_repository import QualityTemplateRepository from app.schemas.quality_templates import ( QualityCheckTemplateCreate, QualityCheckTemplateUpdate, QualityCheckTemplateResponse, QualityCheckTemplateList ) @router.get("/tenants/{tenant_id}/production/quality-templates", response_model=QualityCheckTemplateList) async def get_quality_templates( tenant_id: UUID = Path(...), stage: Optional[str] = Query(None, description="Filter by process stage"), check_type: Optional[str] = Query(None, description="Filter by check type"), is_active: Optional[bool] = Query(True, description="Filter by active status"), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get quality check templates for tenant""" try: repo = QualityTemplateRepository(db) # Convert stage string to ProcessStage enum if provided stage_enum = None if stage: try: stage_enum = ProcessStage(stage) except ValueError: raise HTTPException(status_code=400, detail=f"Invalid stage: {stage}") templates, total = await repo.get_templates_by_tenant( tenant_id=str(tenant_id), stage=stage_enum, check_type=check_type, is_active=is_active, skip=skip, limit=limit ) return QualityCheckTemplateList( templates=[QualityCheckTemplateResponse.from_orm(t) for t in templates], total=total, skip=skip, limit=limit ) except HTTPException: raise except Exception as e: logger.error("Error getting quality templates", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get quality templates") @router.post("/tenants/{tenant_id}/production/quality-templates", response_model=QualityCheckTemplateResponse) async def create_quality_template( template_data: QualityCheckTemplateCreate, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Create a new quality check template""" try: repo = QualityTemplateRepository(db) # Add tenant_id to the template data create_data = template_data.dict() create_data['tenant_id'] = str(tenant_id) template = await repo.create(create_data) return QualityCheckTemplateResponse.from_orm(template) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error creating quality template", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to create quality template") @router.get("/tenants/{tenant_id}/production/quality-templates/{template_id}", response_model=QualityCheckTemplateResponse) async def get_quality_template( tenant_id: UUID = Path(...), template_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get a specific quality check template""" try: repo = QualityTemplateRepository(db) template = await repo.get_by_tenant_and_id(str(tenant_id), template_id) if not template: raise HTTPException(status_code=404, detail="Quality template not found") return QualityCheckTemplateResponse.from_orm(template) except HTTPException: raise except Exception as e: logger.error("Error getting quality template", error=str(e), tenant_id=str(tenant_id), template_id=str(template_id)) raise HTTPException(status_code=500, detail="Failed to get quality template") @router.put("/tenants/{tenant_id}/production/quality-templates/{template_id}", response_model=QualityCheckTemplateResponse) async def update_quality_template( template_data: QualityCheckTemplateUpdate, tenant_id: UUID = Path(...), template_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Update a quality check template""" try: repo = QualityTemplateRepository(db) # First check if template exists and belongs to tenant existing = await repo.get_by_tenant_and_id(str(tenant_id), template_id) if not existing: raise HTTPException(status_code=404, detail="Quality template not found") template = await repo.update(template_id, template_data.dict(exclude_unset=True)) return QualityCheckTemplateResponse.from_orm(template) except HTTPException: raise except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error("Error updating quality template", error=str(e), tenant_id=str(tenant_id), template_id=str(template_id)) raise HTTPException(status_code=500, detail="Failed to update quality template") @router.delete("/tenants/{tenant_id}/production/quality-templates/{template_id}") async def delete_quality_template( tenant_id: UUID = Path(...), template_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Delete a quality check template""" try: repo = QualityTemplateRepository(db) # First check if template exists and belongs to tenant existing = await repo.get_by_tenant_and_id(str(tenant_id), template_id) if not existing: raise HTTPException(status_code=404, detail="Quality template not found") await repo.delete(template_id) return {"message": "Quality template deleted successfully"} except HTTPException: raise except Exception as e: logger.error("Error deleting quality template", error=str(e), tenant_id=str(tenant_id), template_id=str(template_id)) raise HTTPException(status_code=500, detail="Failed to delete quality template") @router.post("/tenants/{tenant_id}/production/quality-templates/{template_id}/duplicate", response_model=QualityCheckTemplateResponse) async def duplicate_quality_template( tenant_id: UUID = Path(...), template_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Duplicate an existing quality check template""" try: repo = QualityTemplateRepository(db) # Get original template original = await repo.get_by_tenant_and_id(str(tenant_id), template_id) if not original: raise HTTPException(status_code=404, detail="Quality template not found") # Create duplicate data duplicate_data = { "tenant_id": original.tenant_id, "name": f"{original.name} (Copy)", "template_code": None, # Will be auto-generated "check_type": original.check_type, "category": original.category, "description": original.description, "instructions": original.instructions, "criteria": original.criteria, "is_required": original.is_required, "is_critical": original.is_critical, "weight": original.weight, "min_value": original.min_value, "max_value": original.max_value, "unit": original.unit, "tolerance_percentage": original.tolerance_percentage, "applicable_stages": original.applicable_stages, "created_by": original.created_by } template = await repo.create(duplicate_data) return QualityCheckTemplateResponse.from_orm(template) except HTTPException: raise except Exception as e: logger.error("Error duplicating quality template", error=str(e), tenant_id=str(tenant_id), template_id=str(template_id)) raise HTTPException(status_code=500, detail="Failed to duplicate quality template")