Improve frontend 5

This commit is contained in:
Urtzi Alfaro
2025-11-20 19:14:49 +01:00
parent 29e6ddcea9
commit 4433b66f25
30 changed files with 3649 additions and 600 deletions

View File

@@ -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):

View File

@@ -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}}
}
}
}