Improve frontend panel de control

This commit is contained in:
Urtzi Alfaro
2025-11-20 22:10:16 +01:00
parent 80298b61b2
commit 2ee94fb4b1
9 changed files with 117 additions and 74 deletions

View File

@@ -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({

View File

@@ -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({
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
{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'}
</span>
{item.status === 'IN_PROGRESS' && (

View File

@@ -35,12 +35,19 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
const [showRawJson, setShowRawJson] = useState(false);
const [rawJson, setRawJson] = useState('');
const [jsonError, setJsonError] = useState<string | null>(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<KeyValueEditorProps> = ({
if (value.trim() === '') {
setPairs([]);
setRawJson('{}');
setIsInitialized(true);
return;
}
jsonObj = JSON.parse(value);
@@ -58,23 +66,28 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
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<KeyValueEditorProps> = ({
};
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) => {

View File

@@ -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}}.",

View File

@@ -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}}.",

View File

@@ -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}}.",

View File

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

View File

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

View File

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