diff --git a/frontend/src/components/dashboard/ActionQueueCard.tsx b/frontend/src/components/dashboard/ActionQueueCard.tsx
index d4a8ea9c..14f132fb 100644
--- a/frontend/src/components/dashboard/ActionQueueCard.tsx
+++ b/frontend/src/components/dashboard/ActionQueueCard.tsx
@@ -94,14 +94,15 @@ function translateKey(
if (key.startsWith('reasoning.')) {
// Remove 'reasoning.' prefix and use reasoning namespace
const translationKey = key.substring('reasoning.'.length);
- return tReasoning(translationKey, { ...processedParams, defaultValue: key });
+ // Use i18next-icu for interpolation with {variable} syntax
+ return String(tReasoning(translationKey, processedParams));
} else if (key.startsWith('action_queue.') || key.startsWith('dashboard.') || key.startsWith('health.')) {
// Use dashboard namespace
- return tDashboard(key, { ...processedParams, defaultValue: key });
+ return String(tDashboard(key, processedParams));
}
// Default to dashboard
- return tDashboard(key, { ...processedParams, defaultValue: key });
+ return String(tDashboard(key, processedParams));
}
function ActionItemCard({
diff --git a/frontend/src/components/dashboard/ProductionTimelineCard.tsx b/frontend/src/components/dashboard/ProductionTimelineCard.tsx
index e09fae77..318980ba 100644
--- a/frontend/src/components/dashboard/ProductionTimelineCard.tsx
+++ b/frontend/src/components/dashboard/ProductionTimelineCard.tsx
@@ -55,7 +55,10 @@ function TimelineItemCard({
translationKey = key.substring('production.'.length);
namespace = 'production';
}
- return { reasoning: t(translationKey, { ...params, ns: namespace, defaultValue: item.reasoning || '' }) };
+ // Use i18next-icu for interpolation with {variable} syntax
+ const fullKey = `${namespace}:${translationKey}`;
+ const result = t(fullKey, params);
+ return { reasoning: String(result || item.reasoning || '') };
} else if (item.reasoning_data) {
return formatBatchAction(item.reasoning_data);
}
@@ -112,11 +115,15 @@ function TimelineItemCard({
{item.status_i18n
- ? t(item.status_i18n.key, {
- ...item.status_i18n.params,
- ns: item.status_i18n.key.startsWith('production.') ? 'production' : 'reasoning',
- defaultValue: item.statusText || 'Status'
- })
+ ? (() => {
+ const statusKey = item.status_i18n.key;
+ const statusNamespace = statusKey.startsWith('production.') ? 'production' : 'reasoning';
+ const statusTranslationKey = statusKey.startsWith('production.')
+ ? statusKey.substring('production.'.length)
+ : (statusKey.startsWith('reasoning.') ? statusKey.substring('reasoning.'.length) : statusKey);
+ const fullStatusKey = `${statusNamespace}:${statusTranslationKey}`;
+ return String(t(fullStatusKey, item.status_i18n.params) || item.statusText || 'Status');
+ })()
: item.statusText || 'Status'}
{item.status === 'IN_PROGRESS' && (
diff --git a/frontend/src/components/ui/KeyValueEditor/KeyValueEditor.tsx b/frontend/src/components/ui/KeyValueEditor/KeyValueEditor.tsx
index 1fbafe0d..6a7c36d7 100644
--- a/frontend/src/components/ui/KeyValueEditor/KeyValueEditor.tsx
+++ b/frontend/src/components/ui/KeyValueEditor/KeyValueEditor.tsx
@@ -35,12 +35,19 @@ export const KeyValueEditor: React.FC = ({
const [showRawJson, setShowRawJson] = useState(false);
const [rawJson, setRawJson] = useState('');
const [jsonError, setJsonError] = useState(null);
+ const [isInitialized, setIsInitialized] = useState(false);
- // Initialize pairs from value
+ // Initialize pairs from value only on first mount or when value changes from external source
useEffect(() => {
+ // Skip if already initialized with internal changes
+ if (isInitialized && pairs.length > 0) {
+ return;
+ }
+
if (!value) {
setPairs([]);
setRawJson('{}');
+ setIsInitialized(true);
return;
}
@@ -51,6 +58,7 @@ export const KeyValueEditor: React.FC = ({
if (value.trim() === '') {
setPairs([]);
setRawJson('{}');
+ setIsInitialized(true);
return;
}
jsonObj = JSON.parse(value);
@@ -58,23 +66,28 @@ export const KeyValueEditor: React.FC = ({
jsonObj = value;
}
- const newPairs: KeyValuePair[] = Object.entries(jsonObj).map(([key, val], index) => ({
- id: `pair-${Date.now()}-${index}`,
- key,
- value: String(val),
- type: detectType(val)
- }));
+ // Only update if there's actual data
+ if (Object.keys(jsonObj).length > 0) {
+ const newPairs: KeyValuePair[] = Object.entries(jsonObj).map(([key, val], index) => ({
+ id: `pair-${Date.now()}-${index}`,
+ key,
+ value: String(val),
+ type: detectType(val)
+ }));
- setPairs(newPairs);
- setRawJson(JSON.stringify(jsonObj, null, 2));
- setJsonError(null);
+ setPairs(newPairs);
+ setRawJson(JSON.stringify(jsonObj, null, 2));
+ setJsonError(null);
+ }
+ setIsInitialized(true);
} catch (error) {
// If parsing fails, treat as empty
setPairs([]);
setRawJson(typeof value === 'string' ? value : '{}');
setJsonError('Invalid JSON');
+ setIsInitialized(true);
}
- }, [value]);
+ }, [value, isInitialized, pairs.length]);
const detectType = (value: any): 'string' | 'number' | 'boolean' => {
if (typeof value === 'boolean') return 'boolean';
@@ -112,7 +125,8 @@ export const KeyValueEditor: React.FC = ({
};
const newPairs = [...pairs, newPair];
setPairs(newPairs);
- onChange?.(pairsToJson(newPairs));
+ // Don't call onChange yet - wait for user to fill in the key
+ // onChange will be called when they type in the key/value
};
const handleRemovePair = (id: string) => {
diff --git a/frontend/src/locales/en/reasoning.json b/frontend/src/locales/en/reasoning.json
index cb2a84a2..f4e7f642 100644
--- a/frontend/src/locales/en/reasoning.json
+++ b/frontend/src/locales/en/reasoning.json
@@ -1,21 +1,21 @@
{
"purchaseOrder": {
- "low_stock_detection": "Low stock for {{supplier_name}}. Current stock of {{product_names_joined}} will run out in {{days_until_stockout}} days.",
- "forecast_demand": "Order scheduled based on {{forecast_period_days}}-day demand forecast for {{product_names_joined}} from {{supplier_name}}.",
- "safety_stock_replenishment": "Replenishing safety stock for {{product_names_joined}} from {{supplier_name}}.",
- "supplier_contract": "Scheduled order per contract with {{supplier_name}}.",
- "seasonal_demand": "Seasonal demand preparation for {{product_names_joined}} from {{supplier_name}}.",
- "production_requirement": "Required for upcoming production batches from {{supplier_name}}.",
- "manual_request": "Manual purchase request for {{product_names_joined}} from {{supplier_name}}."
+ "low_stock_detection": "Low stock for {supplier_name}. Current stock of {product_names_joined} will run out in {days_until_stockout} days.",
+ "forecast_demand": "Order scheduled based on {forecast_period_days}-day demand forecast for {product_names_joined} from {supplier_name}.",
+ "safety_stock_replenishment": "Replenishing safety stock for {product_names_joined} from {supplier_name}.",
+ "supplier_contract": "Scheduled order per contract with {supplier_name}.",
+ "seasonal_demand": "Seasonal demand preparation for {product_names_joined} from {supplier_name}.",
+ "production_requirement": "Required for upcoming production batches from {supplier_name}.",
+ "manual_request": "Manual purchase request for {product_names_joined} from {supplier_name}."
},
"productionBatch": {
- "forecast_demand": "Scheduled based on forecast: {{predicted_demand}} {{product_name}} needed (current stock: {{current_stock}}). Confidence: {{confidence_score}}%.",
- "customer_order": "Customer order for {{customer_name}}: {{order_quantity}} {{product_name}} (Order #{{order_number}}) - delivery {{delivery_date}}.",
- "stock_replenishment": "Stock replenishment for {{product_name}} - current level below minimum.",
- "seasonal_preparation": "Seasonal preparation batch for {{product_name}}.",
- "promotion_event": "Production for promotional event - {{product_name}}.",
- "urgent_order": "Urgent order requiring immediate production of {{product_name}}.",
- "regular_schedule": "Regular scheduled production of {{product_name}}."
+ "forecast_demand": "Scheduled based on forecast: {predicted_demand} {product_name} needed (current stock: {current_stock}). Confidence: {confidence_score}%.",
+ "customer_order": "Customer order for {customer_name}: {order_quantity} {product_name} (Order #{order_number}) - delivery {delivery_date}.",
+ "stock_replenishment": "Stock replenishment for {product_name} - current level below minimum.",
+ "seasonal_preparation": "Seasonal preparation batch for {product_name}.",
+ "promotion_event": "Production for promotional event - {product_name}.",
+ "urgent_order": "Urgent order requiring immediate production of {product_name}.",
+ "regular_schedule": "Regular scheduled production of {product_name}."
},
"consequence": {
"stockout_risk": "Stock-out risk in {{impact_days}} days. Products affected: {{affected_products_joined}}.",
diff --git a/frontend/src/locales/es/reasoning.json b/frontend/src/locales/es/reasoning.json
index fc3d1623..4630345d 100644
--- a/frontend/src/locales/es/reasoning.json
+++ b/frontend/src/locales/es/reasoning.json
@@ -1,21 +1,21 @@
{
"purchaseOrder": {
- "low_stock_detection": "Stock bajo para {{supplier_name}}. El stock actual de {{product_names_joined}} se agotará en {{days_until_stockout}} días.",
- "forecast_demand": "Pedido programado basado en pronóstico de demanda de {{forecast_period_days}} días para {{product_names_joined}} de {{supplier_name}}.",
- "safety_stock_replenishment": "Reposición de stock de seguridad para {{product_names_joined}} de {{supplier_name}}.",
- "supplier_contract": "Pedido programado según contrato con {{supplier_name}}.",
- "seasonal_demand": "Preparación de demanda estacional para {{product_names_joined}} de {{supplier_name}}.",
- "production_requirement": "Requerido para próximos lotes de producción de {{supplier_name}}.",
- "manual_request": "Solicitud de compra manual para {{product_names_joined}} de {{supplier_name}}."
+ "low_stock_detection": "Stock bajo para {supplier_name}. El stock actual de {product_names_joined} se agotará en {days_until_stockout} días.",
+ "forecast_demand": "Pedido programado basado en pronóstico de demanda de {forecast_period_days} días para {product_names_joined} de {supplier_name}.",
+ "safety_stock_replenishment": "Reposición de stock de seguridad para {product_names_joined} de {supplier_name}.",
+ "supplier_contract": "Pedido programado según contrato con {supplier_name}.",
+ "seasonal_demand": "Preparación de demanda estacional para {product_names_joined} de {supplier_name}.",
+ "production_requirement": "Requerido para próximos lotes de producción de {supplier_name}.",
+ "manual_request": "Solicitud de compra manual para {product_names_joined} de {supplier_name}."
},
"productionBatch": {
- "forecast_demand": "Programado según pronóstico: {{predicted_demand}} {{product_name}} necesarios (stock actual: {{current_stock}}). Confianza: {{confidence_score}}%.",
- "customer_order": "Pedido de cliente para {{customer_name}}: {{order_quantity}} {{product_name}} (Pedido #{{order_number}}) - entrega {{delivery_date}}.",
- "stock_replenishment": "Reposición de stock para {{product_name}} - nivel actual por debajo del mínimo.",
- "seasonal_preparation": "Lote de preparación estacional para {{product_name}}.",
- "promotion_event": "Producción para evento promocional - {{product_name}}.",
- "urgent_order": "Pedido urgente que requiere producción inmediata de {{product_name}}.",
- "regular_schedule": "Producción programada regular de {{product_name}}."
+ "forecast_demand": "Programado según pronóstico: {predicted_demand} {product_name} necesarios (stock actual: {current_stock}). Confianza: {confidence_score}%.",
+ "customer_order": "Pedido de cliente para {customer_name}: {order_quantity} {product_name} (Pedido #{order_number}) - entrega {delivery_date}.",
+ "stock_replenishment": "Reposición de stock para {product_name} - nivel actual por debajo del mínimo.",
+ "seasonal_preparation": "Lote de preparación estacional para {product_name}.",
+ "promotion_event": "Producción para evento promocional - {product_name}.",
+ "urgent_order": "Pedido urgente que requiere producción inmediata de {product_name}.",
+ "regular_schedule": "Producción programada regular de {product_name}."
},
"consequence": {
"stockout_risk": "Riesgo de desabastecimiento en {{impact_days}} días. Productos afectados: {{affected_products_joined}}.",
diff --git a/frontend/src/locales/eu/reasoning.json b/frontend/src/locales/eu/reasoning.json
index 6d8d534f..4c923788 100644
--- a/frontend/src/locales/eu/reasoning.json
+++ b/frontend/src/locales/eu/reasoning.json
@@ -1,21 +1,21 @@
{
"purchaseOrder": {
- "low_stock_detection": "{{supplier_name}}-rentzat stock baxua. {{product_names_joined}}-ren egungo stocka {{days_until_stockout}} egunetan amaituko da.",
- "forecast_demand": "{{supplier_name}}-ren {{product_names_joined}}-rentzat {{forecast_period_days}} eguneko eskaera aurreikuspenean oinarritutako eskaera programatua.",
- "safety_stock_replenishment": "{{supplier_name}}-ren {{product_names_joined}}-rentzat segurtasun stockaren birjartzea.",
- "supplier_contract": "{{supplier_name}}-rekin kontratuaren arabera programatutako eskaera.",
- "seasonal_demand": "{{supplier_name}}-ren {{product_names_joined}}-rentzat denboraldiko eskaeraren prestaketa.",
- "production_requirement": "{{supplier_name}}-ren hurrengo ekoizpen loteetarako beharrezkoa.",
- "manual_request": "{{supplier_name}}-ren {{product_names_joined}}-rentzat eskuzko erosketa eskaera."
+ "low_stock_detection": "{supplier_name}-rentzat stock baxua. {product_names_joined}-ren egungo stocka {days_until_stockout} egunetan amaituko da.",
+ "forecast_demand": "{supplier_name}-ren {product_names_joined}-rentzat {forecast_period_days} eguneko eskaera aurreikuspenean oinarritutako eskaera programatua.",
+ "safety_stock_replenishment": "{supplier_name}-ren {product_names_joined}-rentzat segurtasun stockaren birjartzea.",
+ "supplier_contract": "{supplier_name}-rekin kontratuaren arabera programatutako eskaera.",
+ "seasonal_demand": "{supplier_name}-ren {product_names_joined}-rentzat denboraldiko eskaeraren prestaketa.",
+ "production_requirement": "{supplier_name}-ren hurrengo ekoizpen loteetarako beharrezkoa.",
+ "manual_request": "{supplier_name}-ren {product_names_joined}-rentzat eskuzko erosketa eskaera."
},
"productionBatch": {
- "forecast_demand": "Aurreikuspenen arabera programatua: {{predicted_demand}} {{product_name}} behar dira (egungo stocka: {{current_stock}}). Konfiantza: {{confidence_score}}%.",
- "customer_order": "{{customer_name}}-rentzat bezeroaren eskaera: {{order_quantity}} {{product_name}} (Eskaera #{{order_number}}) - entrega {{delivery_date}}.",
- "stock_replenishment": "{{product_name}}-rentzat stockaren birjartzea - egungo maila minimoa baino txikiagoa.",
- "seasonal_preparation": "{{product_name}}-rentzat denboraldiko prestaketa lotea.",
- "promotion_event": "Promozio ekitaldirako ekoizpena - {{product_name}}.",
- "urgent_order": "{{product_name}}-ren berehalako ekoizpena behar duen eskaera larria.",
- "regular_schedule": "{{product_name}}-ren ohiko ekoizpen programatua."
+ "forecast_demand": "Aurreikuspenen arabera programatua: {predicted_demand} {product_name} behar dira (egungo stocka: {current_stock}). Konfiantza: {confidence_score}%.",
+ "customer_order": "{customer_name}-rentzat bezeroaren eskaera: {order_quantity} {product_name} (Eskaera #{order_number}) - entrega {delivery_date}.",
+ "stock_replenishment": "{product_name}-rentzat stockaren birjartzea - egungo maila minimoa baino txikiagoa.",
+ "seasonal_preparation": "{product_name}-rentzat denboraldiko prestaketa lotea.",
+ "promotion_event": "Promozio ekitaldirako ekoizpena - {product_name}.",
+ "urgent_order": "{product_name}-ren berehalako ekoizpena behar duen eskaera larria.",
+ "regular_schedule": "{product_name}-ren ohiko ekoizpen programatua."
},
"consequence": {
"stockout_risk": "Stock amaitzeko arriskua {{impact_days}} egunetan. Produktu kaltetuak: {{affected_products_joined}}.",
diff --git a/services/orchestrator/app/services/dashboard_service.py b/services/orchestrator/app/services/dashboard_service.py
index 55dc7506..f65a7e81 100644
--- a/services/orchestrator/app/services/dashboard_service.py
+++ b/services/orchestrator/app/services/dashboard_service.py
@@ -390,15 +390,30 @@ class DashboardService:
# Calculate urgency based on required delivery date
urgency = self._calculate_po_urgency(po)
- # Get reasoning_data or create default
- reasoning_data = po.get("reasoning_data") or {
- "type": "low_stock_detection",
- "parameters": {
- "supplier_name": po.get('supplier_name', 'Unknown'),
- "product_names": ["Items"],
- "days_until_stockout": 7
+ # Get reasoning_data or create intelligent fallback from PO items
+ reasoning_data = po.get("reasoning_data")
+ if not reasoning_data:
+ # Extract product names from PO line items for better UX
+ product_names = []
+ items = po.get("items", [])
+ for item in items[:5]: # Limit to first 5 items for readability
+ product_name = item.get("product_name") or item.get("name")
+ if product_name:
+ product_names.append(product_name)
+
+ # If no items or product names found, use generic fallback
+ if not product_names:
+ product_names = ["Items"]
+
+ # Create fallback reasoning_data
+ reasoning_data = {
+ "type": "low_stock_detection",
+ "parameters": {
+ "supplier_name": po.get('supplier_name', 'Unknown'),
+ "product_names": product_names,
+ "days_until_stockout": 7
+ }
}
- }
# Get reasoning type and convert to i18n key
reasoning_type = reasoning_data.get('type', 'inventory_replenishment')
diff --git a/services/procurement/app/schemas/purchase_order_schemas.py b/services/procurement/app/schemas/purchase_order_schemas.py
index d4504808..309cd7b6 100644
--- a/services/procurement/app/schemas/purchase_order_schemas.py
+++ b/services/procurement/app/schemas/purchase_order_schemas.py
@@ -168,6 +168,9 @@ class PurchaseOrderResponse(PurchaseOrderBase):
# Additional information
notes: Optional[str] = None
+ # AI/ML reasoning for procurement decisions (JTBD dashboard support)
+ reasoning_data: Optional[Dict[str, Any]] = None
+
# Audit fields
created_at: datetime
updated_at: datetime
diff --git a/services/procurement/scripts/demo/seed_demo_purchase_orders.py b/services/procurement/scripts/demo/seed_demo_purchase_orders.py
index b2cb7111..bfa4ec42 100644
--- a/services/procurement/scripts/demo/seed_demo_purchase_orders.py
+++ b/services/procurement/scripts/demo/seed_demo_purchase_orders.py
@@ -137,7 +137,8 @@ async def create_purchase_order(
try:
# Get product names from items_data
items_list = items_data or []
- product_names = [item.get('product_name', f"Product {i+1}") for i, item in enumerate(items_list)]
+ # CRITICAL FIX: Use 'name' key, not 'product_name', to match items_data structure
+ product_names = [item.get('name', item.get('product_name', f"Product {i+1}")) for i, item in enumerate(items_list)]
if not product_names:
product_names = ["Demo Product"]
@@ -192,8 +193,10 @@ async def create_purchase_order(
if reasoning_data:
try:
po.reasoning_data = reasoning_data
- except Exception:
- pass # Columns don't exist yet
+ logger.debug(f"Set reasoning_data for PO {po_number}: {reasoning_data.get('type', 'unknown')}")
+ except Exception as e:
+ logger.warning(f"Failed to set reasoning_data for PO {po_number}: {e}")
+ pass # Column might not exist yet
# Set approval data if approved
if status in [PurchaseOrderStatus.approved, PurchaseOrderStatus.sent_to_supplier,