Improve frontend 5
This commit is contained in:
@@ -42,6 +42,12 @@ router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/dashboard", tags=["dashbo
|
||||
# Response Models
|
||||
# ============================================================
|
||||
|
||||
class I18nData(BaseModel):
|
||||
"""i18n translation data"""
|
||||
key: str = Field(..., description="i18n translation key")
|
||||
params: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Parameters for translation")
|
||||
|
||||
|
||||
class HeadlineData(BaseModel):
|
||||
"""i18n-ready headline data"""
|
||||
key: str = Field(..., description="i18n translation key")
|
||||
@@ -103,12 +109,12 @@ class OrchestrationSummaryResponse(BaseModel):
|
||||
userActionsRequired: int = Field(..., description="Number of actions needing approval")
|
||||
durationSeconds: Optional[int] = Field(None, description="How long orchestration took")
|
||||
aiAssisted: bool = Field(False, description="Whether AI insights were used")
|
||||
message: Optional[str] = Field(None, description="User-friendly message")
|
||||
message_i18n: Optional[I18nData] = Field(None, description="i18n data for message")
|
||||
|
||||
|
||||
class ActionButton(BaseModel):
|
||||
"""Action button configuration"""
|
||||
label: str
|
||||
label_i18n: I18nData = Field(..., description="i18n data for button label")
|
||||
type: str = Field(..., description="Button type: primary, secondary, tertiary")
|
||||
action: str = Field(..., description="Action identifier")
|
||||
|
||||
@@ -118,10 +124,14 @@ class ActionItem(BaseModel):
|
||||
id: str
|
||||
type: str = Field(..., description="Action type")
|
||||
urgency: str = Field(..., description="Urgency: critical, important, normal")
|
||||
title: str
|
||||
subtitle: str
|
||||
reasoning: str = Field(..., description="Why this action is needed")
|
||||
consequence: str = Field(..., description="What happens if not done")
|
||||
title: Optional[str] = Field(None, description="Legacy field for alerts")
|
||||
title_i18n: Optional[I18nData] = Field(None, description="i18n data for title")
|
||||
subtitle: Optional[str] = Field(None, description="Legacy field for alerts")
|
||||
subtitle_i18n: Optional[I18nData] = Field(None, description="i18n data for subtitle")
|
||||
reasoning: Optional[str] = Field(None, description="Legacy field for alerts")
|
||||
reasoning_i18n: Optional[I18nData] = Field(None, description="i18n data for reasoning")
|
||||
consequence_i18n: I18nData = Field(..., description="i18n data for consequence")
|
||||
reasoning_data: Optional[Dict[str, Any]] = Field(None, description="Structured reasoning data")
|
||||
amount: Optional[float] = Field(None, description="Amount for financial actions")
|
||||
currency: Optional[str] = Field(None, description="Currency code")
|
||||
actions: List[ActionButton]
|
||||
@@ -152,7 +162,9 @@ class ProductionTimelineItem(BaseModel):
|
||||
progress: int = Field(..., ge=0, le=100, description="Progress percentage")
|
||||
readyBy: Optional[str]
|
||||
priority: str
|
||||
reasoning: str
|
||||
reasoning_data: Optional[Dict[str, Any]] = Field(None, description="Structured reasoning data")
|
||||
reasoning_i18n: Optional[I18nData] = Field(None, description="i18n data for reasoning")
|
||||
status_i18n: Optional[I18nData] = Field(None, description="i18n data for status")
|
||||
|
||||
|
||||
class ProductionTimelineResponse(BaseModel):
|
||||
@@ -164,12 +176,17 @@ class ProductionTimelineResponse(BaseModel):
|
||||
pendingBatches: int
|
||||
|
||||
|
||||
class InsightCardI18n(BaseModel):
|
||||
"""i18n data for insight card"""
|
||||
label: I18nData = Field(..., description="i18n data for label")
|
||||
value: I18nData = Field(..., description="i18n data for value")
|
||||
detail: Optional[I18nData] = Field(None, description="i18n data for detail")
|
||||
|
||||
|
||||
class InsightCard(BaseModel):
|
||||
"""Individual insight card"""
|
||||
label: str
|
||||
value: str
|
||||
detail: str
|
||||
color: str = Field(..., description="Color: green, amber, red")
|
||||
i18n: InsightCardI18n = Field(..., description="i18n translation data")
|
||||
|
||||
|
||||
class InsightsResponse(BaseModel):
|
||||
|
||||
@@ -201,28 +201,28 @@ class DashboardService:
|
||||
"""Generate i18n-ready headline based on status"""
|
||||
if status == HealthStatus.GREEN:
|
||||
return {
|
||||
"key": "dashboard.health.headline_green",
|
||||
"key": "health.headline_green",
|
||||
"params": {}
|
||||
}
|
||||
elif status == HealthStatus.YELLOW:
|
||||
if pending_approvals > 0:
|
||||
return {
|
||||
"key": "dashboard.health.headline_yellow_approvals",
|
||||
"key": "health.headline_yellow_approvals",
|
||||
"params": {"count": pending_approvals}
|
||||
}
|
||||
elif critical_alerts > 0:
|
||||
return {
|
||||
"key": "dashboard.health.headline_yellow_alerts",
|
||||
"key": "health.headline_yellow_alerts",
|
||||
"params": {"count": critical_alerts}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"key": "dashboard.health.headline_yellow_general",
|
||||
"key": "health.headline_yellow_general",
|
||||
"params": {}
|
||||
}
|
||||
else: # RED
|
||||
return {
|
||||
"key": "dashboard.health.headline_red",
|
||||
"key": "health.headline_red",
|
||||
"params": {}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,10 @@ class DashboardService:
|
||||
},
|
||||
"userActionsRequired": 0,
|
||||
"status": "no_runs",
|
||||
"message": "No orchestration has been run yet. Click 'Run Daily Planning' to generate your first plan."
|
||||
"message_i18n": {
|
||||
"key": "orchestration.no_runs_message",
|
||||
"params": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Use actual model columns instead of non-existent results attribute
|
||||
@@ -371,10 +374,13 @@ class DashboardService:
|
||||
"title": alert["title"],
|
||||
"subtitle": alert.get("source") or "System Alert",
|
||||
"reasoning": alert.get("description") or "System alert requires attention",
|
||||
"consequence": "Immediate action required to prevent production issues",
|
||||
"consequence_i18n": {
|
||||
"key": "action_queue.consequences.immediate_action",
|
||||
"params": {}
|
||||
},
|
||||
"actions": [
|
||||
{"label": "View Details", "type": "primary", "action": "view_alert"},
|
||||
{"label": "Dismiss", "type": "secondary", "action": "dismiss"}
|
||||
{"label_i18n": {"key": "action_queue.buttons.view_details", "params": {}}, "type": "primary", "action": "view_alert"},
|
||||
{"label_i18n": {"key": "action_queue.buttons.dismiss", "params": {}}, "type": "secondary", "action": "dismiss"}
|
||||
],
|
||||
"estimatedTimeMinutes": 5
|
||||
})
|
||||
@@ -394,20 +400,36 @@ class DashboardService:
|
||||
}
|
||||
}
|
||||
|
||||
# Get reasoning type and convert to i18n key
|
||||
reasoning_type = reasoning_data.get('type', 'inventory_replenishment')
|
||||
reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type)
|
||||
|
||||
actions.append({
|
||||
"id": po["id"],
|
||||
"type": ActionType.APPROVE_PO,
|
||||
"urgency": urgency,
|
||||
"title": f"Purchase Order {po.get('po_number', 'N/A')}",
|
||||
"subtitle": f"Supplier: {po.get('supplier_name', 'Unknown')}",
|
||||
"reasoning": f"Pending approval for {po.get('supplier_name', 'supplier')} - {reasoning_data.get('type', 'inventory replenishment')}",
|
||||
"consequence": "Delayed delivery may impact production schedule",
|
||||
"title_i18n": {
|
||||
"key": "action_queue.titles.purchase_order",
|
||||
"params": {"po_number": po.get('po_number', 'N/A')}
|
||||
},
|
||||
"subtitle_i18n": {
|
||||
"key": "action_queue.titles.supplier",
|
||||
"params": {"supplier_name": po.get('supplier_name', 'Unknown')}
|
||||
},
|
||||
"reasoning_i18n": {
|
||||
"key": reasoning_type_i18n_key,
|
||||
"params": reasoning_data.get('parameters', {})
|
||||
},
|
||||
"consequence_i18n": {
|
||||
"key": "action_queue.consequences.delayed_delivery",
|
||||
"params": {}
|
||||
},
|
||||
"amount": po.get("total_amount", 0),
|
||||
"currency": po.get("currency", "EUR"),
|
||||
"actions": [
|
||||
{"label": "Approve", "type": "primary", "action": "approve"},
|
||||
{"label": "View Details", "type": "secondary", "action": "view_details"},
|
||||
{"label": "Modify", "type": "tertiary", "action": "modify"}
|
||||
{"label_i18n": {"key": "action_queue.buttons.approve", "params": {}}, "type": "primary", "action": "approve"},
|
||||
{"label_i18n": {"key": "action_queue.buttons.view_details", "params": {}}, "type": "secondary", "action": "view_details"},
|
||||
{"label_i18n": {"key": "action_queue.buttons.modify", "params": {}}, "type": "tertiary", "action": "modify"}
|
||||
],
|
||||
"estimatedTimeMinutes": 2
|
||||
})
|
||||
@@ -423,9 +445,12 @@ class DashboardService:
|
||||
"title": step.get("title") or "Complete onboarding step",
|
||||
"subtitle": "Setup incomplete",
|
||||
"reasoning": "Required to unlock full automation",
|
||||
"consequence": step.get("consequence") or "Some features are limited",
|
||||
"consequence_i18n": step.get("consequence_i18n") or {
|
||||
"key": "action_queue.consequences.limited_features",
|
||||
"params": {}
|
||||
},
|
||||
"actions": [
|
||||
{"label": "Complete Setup", "type": "primary", "action": "complete_onboarding"}
|
||||
{"label_i18n": {"key": "action_queue.buttons.complete_setup", "params": {}}, "type": "primary", "action": "complete_onboarding"}
|
||||
],
|
||||
"estimatedTimeMinutes": step.get("estimated_minutes") or 10
|
||||
})
|
||||
@@ -440,6 +465,19 @@ class DashboardService:
|
||||
|
||||
return actions
|
||||
|
||||
def _get_reasoning_type_i18n_key(self, reasoning_type: str) -> str:
|
||||
"""Map reasoning type identifiers to i18n keys"""
|
||||
reasoning_type_map = {
|
||||
"low_stock_detection": "reasoning.types.low_stock_detection",
|
||||
"stockout_prevention": "reasoning.types.stockout_prevention",
|
||||
"forecast_demand": "reasoning.types.forecast_demand",
|
||||
"customer_orders": "reasoning.types.customer_orders",
|
||||
"seasonal_demand": "reasoning.types.seasonal_demand",
|
||||
"inventory_replenishment": "reasoning.types.inventory_replenishment",
|
||||
"production_schedule": "reasoning.types.production_schedule",
|
||||
}
|
||||
return reasoning_type_map.get(reasoning_type, "reasoning.types.other")
|
||||
|
||||
def _calculate_po_urgency(self, po: Dict[str, Any]) -> str:
|
||||
"""Calculate urgency of PO approval based on delivery date"""
|
||||
required_date = po.get("required_delivery_date")
|
||||
@@ -503,6 +541,10 @@ class DashboardService:
|
||||
progress = 100
|
||||
status_icon = "✅"
|
||||
status_text = "COMPLETED"
|
||||
status_i18n = {
|
||||
"key": "production.status.completed",
|
||||
"params": {}
|
||||
}
|
||||
elif status == "IN_PROGRESS":
|
||||
# Calculate progress based on time elapsed
|
||||
if actual_start and planned_end:
|
||||
@@ -513,9 +555,17 @@ class DashboardService:
|
||||
progress = 50
|
||||
status_icon = "🔄"
|
||||
status_text = "IN PROGRESS"
|
||||
status_i18n = {
|
||||
"key": "production.status.in_progress",
|
||||
"params": {}
|
||||
}
|
||||
else:
|
||||
status_icon = "⏰"
|
||||
status_text = "PENDING"
|
||||
status_i18n = {
|
||||
"key": "production.status.pending",
|
||||
"params": {}
|
||||
}
|
||||
|
||||
# Get reasoning_data or create default
|
||||
reasoning_data = batch.get("reasoning_data") or {
|
||||
@@ -527,6 +577,10 @@ class DashboardService:
|
||||
}
|
||||
}
|
||||
|
||||
# Get reasoning type and convert to i18n key
|
||||
reasoning_type = reasoning_data.get('type', 'forecast_demand')
|
||||
reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type)
|
||||
|
||||
timeline.append({
|
||||
"id": batch["id"],
|
||||
"batchNumber": batch.get("batch_number"),
|
||||
@@ -542,8 +596,12 @@ class DashboardService:
|
||||
"progress": progress,
|
||||
"readyBy": planned_end.isoformat() if planned_end else None,
|
||||
"priority": batch.get("priority", "MEDIUM"),
|
||||
"reasoning": batch.get("reasoning") or f"Scheduled based on {reasoning_data.get('type', 'forecast demand')}", # Fallback reasoning text
|
||||
"reasoning_data": reasoning_data # NEW: Structured data for i18n
|
||||
"reasoning_data": reasoning_data, # Structured data for i18n
|
||||
"reasoning_i18n": {
|
||||
"key": reasoning_type_i18n_key,
|
||||
"params": reasoning_data.get('parameters', {})
|
||||
},
|
||||
"status_i18n": status_i18n # i18n for status text
|
||||
})
|
||||
|
||||
# Sort by planned start time
|
||||
@@ -578,19 +636,28 @@ class DashboardService:
|
||||
low_stock_count = inventory_data.get("low_stock_count", 0)
|
||||
out_of_stock_count = inventory_data.get("out_of_stock_count", 0)
|
||||
|
||||
# Determine inventory color
|
||||
if out_of_stock_count > 0:
|
||||
inventory_status = "⚠️ Stock issues"
|
||||
inventory_detail = f"{out_of_stock_count} out of stock"
|
||||
inventory_color = "red"
|
||||
elif low_stock_count > 0:
|
||||
inventory_status = "Low stock"
|
||||
inventory_detail = f"{low_stock_count} alert{'s' if low_stock_count != 1 else ''}"
|
||||
inventory_color = "amber"
|
||||
else:
|
||||
inventory_status = "All stocked"
|
||||
inventory_detail = "No alerts"
|
||||
inventory_color = "green"
|
||||
|
||||
# Create i18n objects for inventory data
|
||||
inventory_i18n = {
|
||||
"status_key": "insights.inventory.stock_issues" if out_of_stock_count > 0 else
|
||||
"insights.inventory.low_stock" if low_stock_count > 0 else
|
||||
"insights.inventory.all_stocked",
|
||||
"status_params": {"count": out_of_stock_count} if out_of_stock_count > 0 else
|
||||
{"count": low_stock_count} if low_stock_count > 0 else {},
|
||||
"detail_key": "insights.inventory.out_of_stock" if out_of_stock_count > 0 else
|
||||
"insights.inventory.alerts" if low_stock_count > 0 else
|
||||
"insights.inventory.no_alerts",
|
||||
"detail_params": {"count": out_of_stock_count} if out_of_stock_count > 0 else
|
||||
{"count": low_stock_count} if low_stock_count > 0 else {}
|
||||
}
|
||||
|
||||
# Waste insight
|
||||
waste_percentage = sustainability_data.get("waste_percentage", 0)
|
||||
waste_target = sustainability_data.get("target_percentage", 5.0)
|
||||
@@ -602,27 +669,71 @@ class DashboardService:
|
||||
|
||||
return {
|
||||
"savings": {
|
||||
"label": "💰 SAVINGS",
|
||||
"value": f"€{weekly_savings:.0f} this week",
|
||||
"detail": f"+{savings_trend:.0f}% vs. last" if savings_trend > 0 else f"{savings_trend:.0f}% vs. last",
|
||||
"color": "green" if savings_trend > 0 else "amber"
|
||||
"color": "green" if savings_trend > 0 else "amber",
|
||||
"i18n": {
|
||||
"label": {
|
||||
"key": "insights.savings.label",
|
||||
"params": {}
|
||||
},
|
||||
"value": {
|
||||
"key": "insights.savings.value_this_week",
|
||||
"params": {"amount": f"{weekly_savings:.0f}"}
|
||||
},
|
||||
"detail": {
|
||||
"key": "insights.savings.detail_vs_last_positive" if savings_trend > 0 else "insights.savings.detail_vs_last_negative",
|
||||
"params": {"percentage": f"{abs(savings_trend):.0f}"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"inventory": {
|
||||
"label": "📦 INVENTORY",
|
||||
"value": inventory_status,
|
||||
"detail": inventory_detail,
|
||||
"color": inventory_color
|
||||
"color": inventory_color,
|
||||
"i18n": {
|
||||
"label": {
|
||||
"key": "insights.inventory.label",
|
||||
"params": {}
|
||||
},
|
||||
"value": {
|
||||
"key": inventory_i18n["status_key"],
|
||||
"params": inventory_i18n["status_params"]
|
||||
},
|
||||
"detail": {
|
||||
"key": inventory_i18n["detail_key"],
|
||||
"params": inventory_i18n["detail_params"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"waste": {
|
||||
"label": "♻️ WASTE",
|
||||
"value": f"{waste_percentage:.1f}% this month",
|
||||
"detail": f"{waste_trend:+.1f}% vs. goal",
|
||||
"color": "green" if waste_trend <= 0 else "amber"
|
||||
"color": "green" if waste_trend <= 0 else "amber",
|
||||
"i18n": {
|
||||
"label": {
|
||||
"key": "insights.waste.label",
|
||||
"params": {}
|
||||
},
|
||||
"value": {
|
||||
"key": "insights.waste.value_this_month",
|
||||
"params": {"percentage": f"{waste_percentage:.1f}"}
|
||||
},
|
||||
"detail": {
|
||||
"key": "insights.waste.detail_vs_goal",
|
||||
"params": {"change": f"{waste_trend:+.1f}"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deliveries": {
|
||||
"label": "🚚 DELIVERIES",
|
||||
"value": f"{deliveries_today} arriving today",
|
||||
"detail": next_delivery or "None scheduled",
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"i18n": {
|
||||
"label": {
|
||||
"key": "insights.deliveries.label",
|
||||
"params": {}
|
||||
},
|
||||
"value": {
|
||||
"key": "insights.deliveries.arriving_today",
|
||||
"params": {"count": deliveries_today}
|
||||
},
|
||||
"detail": {
|
||||
"key": "insights.deliveries.none_scheduled" if not next_delivery else None,
|
||||
"params": {}
|
||||
} if not next_delivery else {"key": None, "params": {"next_delivery": next_delivery}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user