Add whatsapp feature

This commit is contained in:
Urtzi Alfaro
2025-11-13 16:01:08 +01:00
parent d7df2b0853
commit 9bc048d360
74 changed files with 9765 additions and 533 deletions

View File

@@ -40,17 +40,25 @@ router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/dashboard", tags=["dashbo
# Response Models
# ============================================================
class HeadlineData(BaseModel):
"""i18n-ready headline data"""
key: str = Field(..., description="i18n translation key")
params: Dict[str, Any] = Field(default_factory=dict, description="Parameters for translation")
class HealthChecklistItem(BaseModel):
"""Individual item in health checklist"""
icon: str = Field(..., description="Icon name: check, warning, alert")
text: str = Field(..., description="Checklist item text")
text: Optional[str] = Field(None, description="Deprecated: Use textKey instead")
textKey: Optional[str] = Field(None, description="i18n translation key")
textParams: Optional[Dict[str, Any]] = Field(None, description="Parameters for i18n translation")
actionRequired: bool = Field(..., description="Whether action is required")
class BakeryHealthStatusResponse(BaseModel):
"""Overall bakery health status"""
status: str = Field(..., description="Health status: green, yellow, red")
headline: str = Field(..., description="Human-readable status headline")
headline: HeadlineData = Field(..., description="i18n-ready status headline")
lastOrchestrationRun: Optional[str] = Field(None, description="ISO timestamp of last orchestration")
nextScheduledRun: str = Field(..., description="ISO timestamp of next scheduled run")
checklistItems: List[HealthChecklistItem] = Field(..., description="Status checklist")
@@ -83,7 +91,7 @@ class ProductionBatchSummary(BaseModel):
class OrchestrationSummaryResponse(BaseModel):
"""What the orchestrator did for the user"""
runTimestamp: Optional[str] = Field(None, description="When the orchestration ran")
runNumber: Optional[int] = Field(None, description="Run sequence number")
runNumber: Optional[str] = Field(None, description="Run number identifier")
status: str = Field(..., description="Run status")
purchaseOrdersCreated: int = Field(..., description="Number of POs created")
purchaseOrdersSummary: List[PurchaseOrderSummary] = Field(default_factory=list)

View File

@@ -92,13 +92,14 @@ class DashboardService:
if production_delays == 0:
checklist_items.append({
"icon": "check",
"text": "Production on schedule",
"textKey": "dashboard.health.production_on_schedule",
"actionRequired": False
})
else:
checklist_items.append({
"icon": "warning",
"text": f"{production_delays} production batch{'es' if production_delays != 1 else ''} delayed",
"textKey": "dashboard.health.production_delayed",
"textParams": {"count": production_delays},
"actionRequired": True
})
@@ -106,13 +107,14 @@ class DashboardService:
if out_of_stock_count == 0:
checklist_items.append({
"icon": "check",
"text": "All ingredients in stock",
"textKey": "dashboard.health.all_ingredients_in_stock",
"actionRequired": False
})
else:
checklist_items.append({
"icon": "alert",
"text": f"{out_of_stock_count} ingredient{'s' if out_of_stock_count != 1 else ''} out of stock",
"textKey": "dashboard.health.ingredients_out_of_stock",
"textParams": {"count": out_of_stock_count},
"actionRequired": True
})
@@ -120,13 +122,14 @@ class DashboardService:
if pending_approvals == 0:
checklist_items.append({
"icon": "check",
"text": "No pending approvals",
"textKey": "dashboard.health.no_pending_approvals",
"actionRequired": False
})
else:
checklist_items.append({
"icon": "warning",
"text": f"{pending_approvals} purchase order{'s' if pending_approvals != 1 else ''} awaiting approval",
"textKey": "dashboard.health.approvals_awaiting",
"textParams": {"count": pending_approvals},
"actionRequired": True
})
@@ -134,13 +137,14 @@ class DashboardService:
if system_errors == 0 and critical_alerts == 0:
checklist_items.append({
"icon": "check",
"text": "All systems operational",
"textKey": "dashboard.health.all_systems_operational",
"actionRequired": False
})
else:
checklist_items.append({
"icon": "alert",
"text": f"{critical_alerts + system_errors} critical issue{'s' if (critical_alerts + system_errors) != 1 else ''}",
"textKey": "dashboard.health.critical_issues",
"textParams": {"count": critical_alerts + system_errors},
"actionRequired": True
})
@@ -193,19 +197,34 @@ class DashboardService:
status: str,
critical_alerts: int,
pending_approvals: int
) -> str:
"""Generate human-readable headline based on status"""
) -> Dict[str, Any]:
"""Generate i18n-ready headline based on status"""
if status == HealthStatus.GREEN:
return "Your bakery is running smoothly"
return {
"key": "dashboard.health.headline_green",
"params": {}
}
elif status == HealthStatus.YELLOW:
if pending_approvals > 0:
return f"Please review {pending_approvals} pending approval{'s' if pending_approvals != 1 else ''}"
return {
"key": "dashboard.health.headline_yellow_approvals",
"params": {"count": pending_approvals}
}
elif critical_alerts > 0:
return f"You have {critical_alerts} alert{'s' if critical_alerts != 1 else ''} needing attention"
return {
"key": "dashboard.health.headline_yellow_alerts",
"params": {"count": critical_alerts}
}
else:
return "Some items need your attention"
return {
"key": "dashboard.health.headline_yellow_general",
"params": {}
}
else: # RED
return "Critical issues require immediate action"
return {
"key": "dashboard.health.headline_red",
"params": {}
}
async def _get_last_orchestration_run(self, tenant_id: str) -> Optional[Dict[str, Any]]:
"""Get the most recent orchestration run"""
@@ -286,18 +305,16 @@ class DashboardService:
"message": "No orchestration has been run yet. Click 'Run Daily Planning' to generate your first plan."
}
# Parse results from JSONB
results = run.results or {}
# Use actual model columns instead of non-existent results attribute
po_count = run.purchase_orders_created or 0
batch_count = run.production_batches_created or 0
forecasts_count = run.forecasts_generated or 0
# Extract step results
step_results = results.get("steps", {})
forecasting_step = step_results.get("1", {})
production_step = step_results.get("2", {})
procurement_step = step_results.get("3", {})
# Get metadata if available
run_metadata = run.run_metadata or {}
# Count created entities
po_count = procurement_step.get("purchase_orders_created", 0)
batch_count = production_step.get("production_batches_created", 0)
# Extract forecast data if available
forecast_data = run.forecast_data or {}
# Get detailed summaries (these would come from the actual services in real implementation)
# For now, provide structure that the frontend expects
@@ -311,14 +328,14 @@ class DashboardService:
"productionBatchesCreated": batch_count,
"productionBatchesSummary": [], # Will be filled by separate service calls
"reasoningInputs": {
"customerOrders": forecasting_step.get("orders_analyzed", 0),
"historicalDemand": forecasting_step.get("success", False),
"inventoryLevels": procurement_step.get("success", False),
"aiInsights": results.get("ai_insights_used", False)
"customerOrders": forecasts_count,
"historicalDemand": run.forecasting_status == "success",
"inventoryLevels": run.procurement_status == "success",
"aiInsights": (run.ai_insights_generated or 0) > 0
},
"userActionsRequired": po_count, # POs need approval
"durationSeconds": run.duration_seconds,
"aiAssisted": results.get("ai_insights_used", False)
"aiAssisted": (run.ai_insights_generated or 0) > 0
}
async def get_action_queue(