diff --git a/frontend/src/components/dashboard/NetworkSummaryCards.tsx b/frontend/src/components/dashboard/NetworkSummaryCards.tsx index bbf2ed50..768f1612 100644 --- a/frontend/src/components/dashboard/NetworkSummaryCards.tsx +++ b/frontend/src/components/dashboard/NetworkSummaryCards.tsx @@ -31,9 +31,9 @@ interface NetworkSummaryCardsProps { isLoading: boolean; } -const NetworkSummaryCards: React.FC = ({ - data, - isLoading +const NetworkSummaryCards: React.FC = ({ + data, + isLoading }) => { const { t } = useTranslation('dashboard'); @@ -43,10 +43,10 @@ const NetworkSummaryCards: React.FC = ({ {[...Array(5)].map((_, index) => ( - + -
+
))} @@ -56,7 +56,7 @@ const NetworkSummaryCards: React.FC = ({ if (!data) { return ( -
+
{t('enterprise.no_network_data')}
); @@ -67,16 +67,16 @@ const NetworkSummaryCards: React.FC = ({ {/* Network Outlets Card */} - + {t('enterprise.network_outlets')} - + -
+
{data.child_tenant_count}
-

+

{t('enterprise.outlets_in_network')}

@@ -85,16 +85,16 @@ const NetworkSummaryCards: React.FC = ({ {/* Network Sales Card */} - + {t('enterprise.network_sales')} - + -
+
{formatCurrency(data.network_sales_30d, 'EUR')}
-

+

{t('enterprise.last_30_days')}

@@ -103,16 +103,16 @@ const NetworkSummaryCards: React.FC = ({ {/* Production Volume Card */} - + {t('enterprise.production_volume')} - + -
+
{new Intl.NumberFormat('es-ES').format(data.production_volume_30d)} kg
-

+

{t('enterprise.last_30_days')}

@@ -121,16 +121,16 @@ const NetworkSummaryCards: React.FC = ({ {/* Pending Internal Transfers Card */} - + {t('enterprise.pending_orders')} - + -
+
{data.pending_internal_transfers_count}
-

+

{t('enterprise.internal_transfers')}

@@ -139,16 +139,16 @@ const NetworkSummaryCards: React.FC = ({ {/* Active Shipments Card */} - + {t('enterprise.active_shipments')} - + -
+
{data.active_shipments_count}
-

+

{t('enterprise.today')}

diff --git a/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx b/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx index 37f636c8..fe666846 100644 --- a/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx +++ b/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx @@ -133,6 +133,10 @@ function getActionLabelKey(actionType: string, metadata?: Record): key: 'alerts:actions.reject_po', extractParams: () => ({}) }, + 'modify_po': { + key: 'alerts:actions.modify_po', + extractParams: () => ({}) + }, 'view_po_details': { key: 'alerts:actions.view_po_details', extractParams: () => ({}) @@ -252,6 +256,17 @@ function ActionCard({ alert, showEscalationBadge = false, onActionSuccess, onAct // Determine if this is a critical alert that needs stronger visual treatment const isCritical = alert.priority_level === 'critical' || alert.priority_level === 'CRITICAL'; + // Detect if this is an AI-generated PO + const isAIGeneratedPO = alert.event_type?.includes('po') && + (alert.orchestrator_context?.already_addressed || + alert.orchestrator_context?.action_type === 'create_po' || + alert.event_metadata?.source === 'orchestrator' || + alert.event_metadata?.auto_generated === true || + alert.event_metadata?.reasoning_data?.metadata?.ai_assisted === true || + alert.event_metadata?.reasoning_data?.metadata?.trigger_source === 'orchestrator_auto' || + alert.ai_reasoning?.details?.metadata?.ai_assisted === true || + alert.ai_reasoning?.details?.metadata?.trigger_source === 'orchestrator_auto'); + // Extract reasoning from alert using new rendering utility const reasoningText = renderAIReasoning(alert, t) || ''; @@ -294,6 +309,16 @@ function ActionCard({ alert, showEscalationBadge = false, onActionSuccess, onAct )}
+ {/* AI-Generated PO Badge */} + {isAIGeneratedPO && ( +
+
+ + {t('alerts:orchestration.ai_generated_po')} +
+
+ )} + {/* Escalation Badge Details */} {showEscalationBadge && } @@ -580,6 +605,7 @@ export function UnifiedActionQueueCard({ // PO Details Modal state const [isPODetailsModalOpen, setIsPODetailsModalOpen] = useState(false); const [selectedPOId, setSelectedPOId] = useState(null); + const [poModalMode, setPOModalMode] = useState<'view' | 'edit'>('view'); // Subscribe to SSE notifications for real-time alerts const { notifications, isConnected } = useEventNotifications(); @@ -638,13 +664,14 @@ export function UnifiedActionQueueCard({ }; }, [actionQueue]); - // Listen for PO details modal open events + // Listen for PO details modal open events (view mode) useEffect(() => { const handlePODetailsOpen = (event: CustomEvent) => { - const { po_id } = event.detail; + const { po_id, mode } = event.detail; if (po_id) { setSelectedPOId(po_id); + setPOModalMode(mode || 'view'); setIsPODetailsModalOpen(true); } }; @@ -655,6 +682,24 @@ export function UnifiedActionQueueCard({ }; }, []); + // Listen for PO edit modal open events (edit mode) + useEffect(() => { + const handlePOEditOpen = (event: CustomEvent) => { + const { po_id, mode } = event.detail; + + if (po_id) { + setSelectedPOId(po_id); + setPOModalMode(mode || 'edit'); + setIsPODetailsModalOpen(true); + } + }; + + window.addEventListener('po:open-edit' as any, handlePOEditOpen); + return () => { + window.removeEventListener('po:open-edit' as any, handlePOEditOpen); + }; + }, []); + // Create a stable identifier for notifications to prevent infinite re-renders // Only recalculate when the actual notification IDs and read states change const notificationKey = useMemo(() => { @@ -976,7 +1021,7 @@ export function UnifiedActionQueueCard({ /> )} - {/* PO Details Modal - Opened by "Ver detalles" action */} + {/* PO Details Modal - Opened by "Ver detalles" or "Modificar PO" action */} {isPODetailsModalOpen && selectedPOId && tenantId && ( { setIsPODetailsModalOpen(false); setSelectedPOId(null); + setPOModalMode('view'); }} showApprovalActions={true} - initialMode="view" + initialMode={poModalMode} /> )}
diff --git a/frontend/src/locales/en/alerts.json b/frontend/src/locales/en/alerts.json index 5b26018c..a3d30885 100644 --- a/frontend/src/locales/en/alerts.json +++ b/frontend/src/locales/en/alerts.json @@ -24,6 +24,7 @@ "actions": { "approve_po": "Approve €{amount} order", "reject_po": "Reject order", + "modify_po": "Modify order", "view_po_details": "View details", "call_supplier": "Call {supplier} ({phone})", "see_reasoning": "See full reasoning", @@ -45,7 +46,8 @@ "estimated_impact": "Estimated Impact", "impact_description": "Savings from prevented rush orders, stockouts, and waste", "last_run": "Last run", - "what_ai_did": "What AI did for you" + "what_ai_did": "What AI did for you", + "ai_generated_po": "AI-Generated PO" }, "no_reasoning_available": "No reasoning available", "metrics": { diff --git a/frontend/src/locales/es/alerts.json b/frontend/src/locales/es/alerts.json index cc41030d..5a0f72b4 100644 --- a/frontend/src/locales/es/alerts.json +++ b/frontend/src/locales/es/alerts.json @@ -24,6 +24,7 @@ "actions": { "approve_po": "Aprobar pedido €{amount}", "reject_po": "Rechazar pedido", + "modify_po": "Modificar pedido", "view_po_details": "Ver detalles", "call_supplier": "Llamar a {supplier} ({phone})", "see_reasoning": "Ver razonamiento completo", @@ -45,7 +46,8 @@ "estimated_impact": "Impacto Estimado", "impact_description": "Ahorros por prevención de pedidos urgentes, faltas de stock y desperdicios", "last_run": "Última ejecución", - "what_ai_did": "Lo que hizo la IA por ti" + "what_ai_did": "Lo que hizo la IA por ti", + "ai_generated_po": "PO Generada por IA" }, "ai_reasoning_label": "Razonamiento de IA", "no_reasoning_available": "No hay razonamiento disponible", diff --git a/frontend/src/pages/app/EnterpriseDashboardPage.tsx b/frontend/src/pages/app/EnterpriseDashboardPage.tsx index ca448600..5af90b5e 100644 --- a/frontend/src/pages/app/EnterpriseDashboardPage.tsx +++ b/frontend/src/pages/app/EnterpriseDashboardPage.tsx @@ -200,22 +200,22 @@ const EnterpriseDashboardPage: React.FC = ({ tenan return ( -
+
{/* Breadcrumb / Return to Network Banner */} {enterpriseState.selectedOutletId && !enterpriseState.isNetworkView && ( -
+
- +
- Network Overview - - {enterpriseState.selectedOutletName} + Network Overview + + {enterpriseState.selectedOutletName}
{enterpriseState.networkMetrics && (
+ style={{ borderColor: 'var(--border-primary)' }}>
- Network Average Sales: - €{enterpriseState.networkMetrics.averageSales.toLocaleString()} + Network Average Sales: + €{enterpriseState.networkMetrics.averageSales.toLocaleString()}
- Total Outlets: - {enterpriseState.networkMetrics.childCount} + Total Outlets: + {enterpriseState.networkMetrics.childCount}
- Network Total: - €{enterpriseState.networkMetrics.totalSales.toLocaleString()} + Network Total: + €{enterpriseState.networkMetrics.totalSales.toLocaleString()}
)} @@ -262,10 +262,10 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
-

+

{t('enterprise.network_dashboard')}

-

+

{t('enterprise.network_summary_description')}

@@ -288,16 +288,16 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
- + {t('enterprise.distribution_map')}
- + setSelectedDate(e.target.value)} - className="border rounded px-2 py-1 text-sm" + className="border border-[var(--border-primary)] rounded-md px-2 py-1 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]" />
@@ -308,7 +308,7 @@ const EnterpriseDashboardPage: React.FC = ({ tenan shipments={distributionOverview.status_counts} /> ) : ( -
+
{t('enterprise.no_distribution_data')}
)} @@ -321,14 +321,14 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
- + {t('enterprise.outlet_performance')}
setSelectedPeriod(Number(e.target.value))} - className="border rounded px-2 py-1 text-sm" + className="border border-[var(--border-primary)] rounded-md px-2 py-1 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]" > @@ -354,7 +354,7 @@ const EnterpriseDashboardPage: React.FC = ({ tenan onOutletClick={handleOutletClick} /> ) : ( -
+
{t('enterprise.no_performance_data')}
)} @@ -367,121 +367,105 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
- + {t('enterprise.network_forecast')} {forecastSummary && forecastSummary.aggregated_forecasts ? (
{/* Total Demand Card */} -
-
-
- + + +
+
+ +
+

+ {t('enterprise.total_demand')} +

-

- {t('enterprise.total_demand')} -

-
-

- {Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) => - total + Object.values(day).reduce((dayTotal: number, product: any) => - dayTotal + (product.predicted_demand || 0), 0), 0 - ).toLocaleString()} -

-
+

+ {Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) => + total + Object.values(day).reduce((dayTotal: number, product: any) => + dayTotal + (product.predicted_demand || 0), 0), 0 + ).toLocaleString()} +

+ + {/* Days Forecast Card */} -
-
-
- + + +
+
+ +
+

+ {t('enterprise.days_forecast')} +

-

- {t('enterprise.days_forecast')} -

-
-

- {forecastSummary.days_forecast || 7} -

-
+

+ {forecastSummary.days_forecast || 7} +

+ + {/* Average Daily Demand Card */} -
-
-
- + + +
+
+ +
+

+ {t('enterprise.avg_daily_demand')} +

-

- {t('enterprise.avg_daily_demand')} -

-
-

- {forecastSummary.aggregated_forecasts - ? Math.round(Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) => - total + Object.values(day).reduce((dayTotal: number, product: any) => - dayTotal + (product.predicted_demand || 0), 0), 0) / - Object.keys(forecastSummary.aggregated_forecasts).length - ).toLocaleString() - : 0} -

-
+

+ {forecastSummary.aggregated_forecasts + ? Math.round(Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) => + total + Object.values(day).reduce((dayTotal: number, product: any) => + dayTotal + (product.predicted_demand || 0), 0), 0) / + Object.keys(forecastSummary.aggregated_forecasts).length + ).toLocaleString() + : 0} +

+ + {/* Last Updated Card */} -
-
-
- + + +
+
+ +
+

+ {t('enterprise.last_updated')} +

-

- {t('enterprise.last_updated')} -

-
-

- {forecastSummary.last_updated ? - new Date(forecastSummary.last_updated).toLocaleTimeString() : - 'N/A'} -

-
+

+ {forecastSummary.last_updated ? + new Date(forecastSummary.last_updated).toLocaleTimeString() : + 'N/A'} +

+ +
) : ( -
+
{t('enterprise.no_forecast_data')}
)} @@ -494,10 +478,10 @@ const EnterpriseDashboardPage: React.FC = ({ tenan
- -

Agregar Punto de Venta

+ +

Agregar Punto de Venta

-

Añadir un nuevo outlet a la red enterprise

+

Añadir un nuevo outlet a la red enterprise