Improve frontend panel de control
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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,6 +66,8 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
|
||||
jsonObj = value;
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -68,13 +78,16 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
|
||||
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) => {
|
||||
|
||||
@@ -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}}.",
|
||||
|
||||
@@ -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}}.",
|
||||
|
||||
@@ -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}}.",
|
||||
|
||||
@@ -390,12 +390,27 @@ 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 {
|
||||
# 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": ["Items"],
|
||||
"product_names": product_names,
|
||||
"days_until_stockout": 7
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user