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,