diff --git a/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx b/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx
new file mode 100644
index 00000000..f9b9cfc3
--- /dev/null
+++ b/frontend/src/components/dashboard/UnifiedActionQueueCard.tsx
@@ -0,0 +1,772 @@
+// ================================================================
+// frontend/src/components/dashboard/UnifiedActionQueueCard.tsx
+// ================================================================
+/**
+ * Unified Action Queue Card - Time-Based Grouping
+ *
+ * NEW implementation replacing the old ActionQueueCard.
+ * Groups all action-needed alerts into URGENT/TODAY/WEEK sections.
+ *
+ * Features:
+ * - Time-based grouping (<6h, <24h, <7d)
+ * - Escalation badges for aged actions
+ * - Embedded delivery actions
+ * - Collapsible sections
+ * - Smart action handlers
+ */
+
+import React, { useState, useMemo, useEffect } from 'react';
+import {
+ AlertCircle,
+ Clock,
+ ChevronDown,
+ ChevronUp,
+ Zap,
+ AlertTriangle,
+ Calendar,
+ TrendingUp,
+ Package,
+ Truck,
+ Bot,
+ Wifi,
+ WifiOff,
+ CheckCircle,
+ XCircle,
+ Phone,
+} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import { UnifiedActionQueue, EnrichedAlert } from '../../api/hooks/newDashboard';
+import { useSmartActionHandler, mapActionVariantToButton } from '../../utils/smartActionHandlers';
+import { Button } from '../ui/Button';
+import { useNotifications } from '../../hooks/useNotifications';
+import { StockReceiptModal } from './StockReceiptModal';
+import { ReasoningModal } from '../domain/dashboard/ReasoningModal';
+
+interface UnifiedActionQueueCardProps {
+ actionQueue: UnifiedActionQueue;
+ loading?: boolean;
+ tenantId?: string;
+}
+
+interface ActionCardProps {
+ alert: EnrichedAlert;
+ showEscalationBadge?: boolean;
+ onActionSuccess?: () => void;
+ onActionError?: (error: string) => void;
+}
+
+function getUrgencyColor(priorityLevel: string): {
+ bg: string;
+ border: string;
+ text: string;
+} {
+ switch (priorityLevel.toUpperCase()) {
+ case 'CRITICAL':
+ return {
+ bg: 'var(--color-error-50)',
+ border: 'var(--color-error-300)',
+ text: 'var(--color-error-900)',
+ };
+ case 'IMPORTANT':
+ return {
+ bg: 'var(--color-warning-50)',
+ border: 'var(--color-warning-300)',
+ text: 'var(--color-warning-900)',
+ };
+ default:
+ return {
+ bg: 'var(--color-info-50)',
+ border: 'var(--color-info-300)',
+ text: 'var(--color-info-900)',
+ };
+ }
+}
+
+function EscalationBadge({ alert }: { alert: EnrichedAlert }) {
+ const { t } = useTranslation('alerts');
+ const escalation = alert.alert_metadata?.escalation;
+
+ if (!escalation || escalation.boost_applied === 0) return null;
+
+ const hoursPending = alert.urgency_context?.time_until_consequence_hours
+ ? Math.round(alert.urgency_context.time_until_consequence_hours)
+ : null;
+
+ return (
+
+
+
+ {t('escalated')} +{escalation.boost_applied}
+ {hoursPending && ` (${hoursPending}h pending)`}
+
+
+ );
+}
+
+function ActionCard({ alert, showEscalationBadge = false, onActionSuccess, onActionError }: ActionCardProps) {
+ const [expanded, setExpanded] = useState(false);
+ const [loadingAction, setLoadingAction] = useState(null);
+ const [actionCompleted, setActionCompleted] = useState(false);
+ const { t } = useTranslation('alerts');
+ const colors = getUrgencyColor(alert.priority_level);
+
+ // Action handler with callbacks
+ const actionHandler = useSmartActionHandler({
+ onSuccess: (alertId) => {
+ setLoadingAction(null);
+ setActionCompleted(true);
+ onActionSuccess?.();
+
+ // Auto-hide completion state after 2 seconds
+ setTimeout(() => {
+ setActionCompleted(false);
+ }, 2000);
+ },
+ onError: (error) => {
+ setLoadingAction(null);
+ onActionError?.(error);
+ },
+ });
+
+ // Get icon based on alert type
+ const getAlertIcon = () => {
+ if (!alert.alert_type) return AlertCircle;
+ if (alert.alert_type.includes('delivery')) return Truck;
+ if (alert.alert_type.includes('production')) return Package;
+ if (alert.alert_type.includes('procurement') || alert.alert_type.includes('po')) return Calendar;
+ return AlertCircle;
+ };
+
+ const AlertIcon = getAlertIcon();
+
+ // Get actions from alert
+ const alertActions = alert.actions || [];
+
+ // Get icon for action type
+ const getActionIcon = (actionType: string) => {
+ if (actionType.includes('approve')) return CheckCircle;
+ if (actionType.includes('reject') || actionType.includes('cancel')) return XCircle;
+ if (actionType.includes('call')) return Phone;
+ return AlertCircle; // Default fallback icon instead of null
+ };
+
+ // Determine if this is a critical alert that needs stronger visual treatment
+ const isCritical = alert.priority_level === 'CRITICAL';
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ {alert.title}
+
+ {/* Only show escalation badge if applicable */}
+ {showEscalationBadge && alert.alert_metadata?.escalation && (
+
+
+ AGED
+
+
+ )}
+
+
+ {/* Escalation Badge Details */}
+ {showEscalationBadge &&
}
+
+ {/* Message */}
+
+ {alert.message}
+
+
+ {/* Context Badges - Matching Health Hero Style */}
+ {(alert.business_impact || alert.urgency_context || alert.type_class === 'prevented_issue') && (
+
+ {alert.business_impact?.financial_impact_eur && (
+
+
+ €{alert.business_impact.financial_impact_eur.toFixed(0)} at risk
+
+ )}
+ {alert.urgency_context?.time_until_consequence_hours && (
+
+
+ {Math.round(alert.urgency_context.time_until_consequence_hours)}h left
+
+ )}
+ {alert.type_class === 'prevented_issue' && (
+
+
+ AI handled
+
+ )}
+
+ )}
+
+ {/* AI Reasoning (expandable) */}
+ {alert.ai_reasoning_summary && (
+ <>
+
+
+ {expanded && (
+
+
+
+
+ {alert.ai_reasoning_summary}
+
+
+
+ )}
+ >
+ )}
+
+ {/* Smart Actions - Improved with loading states and icons */}
+ {alertActions.length > 0 && !actionCompleted && (
+
+ {alertActions.map((action, idx) => {
+ const buttonVariant = mapActionVariantToButton(action.variant);
+ const isPrimary = action.variant === 'primary';
+ const ActionIcon = isPrimary ? getActionIcon(action.type) : null;
+ const isLoading = loadingAction === action.type;
+
+ return (
+ : undefined}
+ onClick={async () => {
+ setLoadingAction(action.type);
+ await actionHandler.handleAction(action as any, alert.id);
+ }}
+ className={`${isPrimary ? 'font-semibold' : ''} sm:w-auto w-full`}
+ >
+ {action.label}
+ {action.estimated_time_minutes && !isLoading && (
+ ({action.estimated_time_minutes}m)
+ )}
+
+ );
+ })}
+
+ )}
+
+ {/* Action Completed State */}
+ {actionCompleted && (
+
+
+ Action completed successfully
+
+ )}
+
+
+
+ );
+}
+
+interface SectionProps {
+ title: string;
+ icon: React.ElementType;
+ iconColor: string;
+ alerts: EnrichedAlert[];
+ count: number;
+ defaultExpanded?: boolean;
+ showEscalationBadges?: boolean;
+ onActionSuccess?: () => void;
+ onActionError?: (error: string) => void;
+}
+
+function ActionSection({
+ title,
+ icon: RawIcon,
+ iconColor,
+ alerts,
+ count,
+ defaultExpanded = true,
+ showEscalationBadges = false,
+ onActionSuccess,
+ onActionError,
+}: SectionProps) {
+ const [expanded, setExpanded] = useState(defaultExpanded);
+
+ // Safely handle the icon to prevent React error #310
+ const Icon = (RawIcon && typeof RawIcon === 'function')
+ ? RawIcon
+ : AlertCircle;
+
+ if (count === 0) return null;
+
+ return (
+
+ {/* Section Header */}
+
+
+ {/* Section Content */}
+ {expanded && (
+
+ {alerts.map((alert) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export function UnifiedActionQueueCard({
+ actionQueue,
+ loading,
+ tenantId,
+}: UnifiedActionQueueCardProps) {
+ const { t } = useTranslation(['alerts', 'dashboard']);
+ const navigate = useNavigate();
+ const [toastMessage, setToastMessage] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
+
+
+ // Show toast notification
+ useEffect(() => {
+ if (toastMessage) {
+ const timer = setTimeout(() => {
+ setToastMessage(null);
+ }, 3000);
+ return () => clearTimeout(timer);
+ }
+ }, [toastMessage]);
+
+ // Callbacks for action success/error
+ const handleActionSuccess = () => {
+ setToastMessage({ type: 'success', message: t('alerts:action_success', { defaultValue: 'Action completed successfully' }) });
+ };
+
+ const handleActionError = (error: string) => {
+ setToastMessage({ type: 'error', message: error || t('alerts:action_error', { defaultValue: 'Action failed' }) });
+ };
+
+ // StockReceiptModal state
+ const [isStockReceiptModalOpen, setIsStockReceiptModalOpen] = useState(false);
+ const [stockReceiptData, setStockReceiptData] = useState<{
+ receipt: any;
+ mode: 'create' | 'edit';
+ } | null>(null);
+
+ // ReasoningModal state
+ const [reasoningModalOpen, setReasoningModalOpen] = useState(false);
+ const [reasoningData, setReasoningData] = useState(null);
+
+ // Subscribe to SSE notifications for real-time alerts
+ const { notifications, isConnected } = useNotifications();
+
+ // Listen for stock receipt modal open events
+ useEffect(() => {
+ const handleStockReceiptOpen = (event: CustomEvent) => {
+ const { receipt_id, po_id, tenant_id, mode } = event.detail;
+
+ // Build receipt object from event data
+ const receipt = {
+ id: receipt_id,
+ po_id: po_id,
+ tenant_id: tenant_id || tenantId,
+ line_items: [],
+ };
+
+ setStockReceiptData({ receipt, mode });
+ setIsStockReceiptModalOpen(true);
+ };
+
+ window.addEventListener('stock-receipt:open' as any, handleStockReceiptOpen);
+ return () => {
+ window.removeEventListener('stock-receipt:open' as any, handleStockReceiptOpen);
+ };
+ }, [tenantId]);
+
+ // Listen for reasoning modal open events
+ useEffect(() => {
+ const handleReasoningOpen = (event: CustomEvent) => {
+ const { action_id, reasoning, po_id, batch_id } = event.detail;
+
+ // Find the alert to get full context
+ const alert = [
+ ...actionQueue.urgent,
+ ...actionQueue.today,
+ ...actionQueue.week
+ ].find(a => a.id === action_id);
+
+ setReasoningData({
+ action_id,
+ po_id,
+ batch_id,
+ reasoning: reasoning || alert?.ai_reasoning_summary,
+ title: alert?.title,
+ ai_reasoning_summary: alert?.ai_reasoning_summary,
+ business_impact: alert?.business_impact,
+ urgency_context: alert?.urgency_context,
+ });
+ setReasoningModalOpen(true);
+ };
+
+ window.addEventListener('reasoning:show' as any, handleReasoningOpen);
+ return () => {
+ window.removeEventListener('reasoning:show' as any, handleReasoningOpen);
+ };
+ }, [actionQueue]);
+
+ // 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(() => {
+ if (!notifications || notifications.length === 0) return 'empty';
+
+ const key = notifications
+ .filter(n => n.type_class === 'action_needed' && !n.read)
+ .map(n => n.id)
+ .sort()
+ .join(',');
+
+ console.log('🔵 [UnifiedActionQueueCard] notificationKey recalculated:', key);
+ return key;
+ }, [notifications]);
+
+ // Merge API action queue with SSE action-needed alerts
+ const mergedActionQueue = useMemo(() => {
+ if (!actionQueue) return null;
+
+ // Filter SSE notifications to only action_needed alerts
+ // Guard against undefined notifications array
+ const sseActionAlerts = (notifications || []).filter(
+ n => n.type_class === 'action_needed' && !n.read
+ );
+
+ // Create a set of existing alert IDs from API data
+ const existingIds = new Set([
+ ...actionQueue.urgent.map(a => a.id),
+ ...actionQueue.today.map(a => a.id),
+ ...actionQueue.week.map(a => a.id),
+ ]);
+
+ // Filter out SSE alerts that already exist in API data (deduplicate)
+ const newSSEAlerts = sseActionAlerts.filter(alert => !existingIds.has(alert.id));
+
+ // Helper function to categorize alerts by urgency
+ const categorizeByUrgency = (alert: any): 'urgent' | 'today' | 'week' => {
+ const now = new Date();
+ const urgencyContext = alert.urgency_context;
+ const deadline = urgencyContext?.deadline ? new Date(urgencyContext.deadline) : null;
+
+ if (!deadline) {
+ // No deadline: categorize by priority level
+ if (alert.priority_level === 'CRITICAL') return 'urgent';
+ if (alert.priority_level === 'IMPORTANT') return 'today';
+ return 'week';
+ }
+
+ const hoursUntilDeadline = (deadline.getTime() - now.getTime()) / (1000 * 60 * 60);
+
+ if (hoursUntilDeadline < 6 || alert.priority_level === 'CRITICAL') {
+ return 'urgent';
+ } else if (hoursUntilDeadline < 24) {
+ return 'today';
+ } else {
+ return 'week';
+ }
+ };
+
+ // Categorize new SSE alerts
+ const categorizedSSEAlerts = {
+ urgent: newSSEAlerts.filter(a => categorizeByUrgency(a) === 'urgent'),
+ today: newSSEAlerts.filter(a => categorizeByUrgency(a) === 'today'),
+ week: newSSEAlerts.filter(a => categorizeByUrgency(a) === 'week'),
+ };
+
+ // Merge and sort by priority_score (descending)
+ const sortByPriority = (alerts: any[]) =>
+ alerts.sort((a, b) => (b.priority_score || 0) - (a.priority_score || 0));
+
+ const merged = {
+ urgent: sortByPriority([...actionQueue.urgent, ...categorizedSSEAlerts.urgent]),
+ today: sortByPriority([...actionQueue.today, ...categorizedSSEAlerts.today]),
+ week: sortByPriority([...actionQueue.week, ...categorizedSSEAlerts.week]),
+ urgentCount: actionQueue.urgentCount + categorizedSSEAlerts.urgent.length,
+ todayCount: actionQueue.todayCount + categorizedSSEAlerts.today.length,
+ weekCount: actionQueue.weekCount + categorizedSSEAlerts.week.length,
+ totalActions: actionQueue.totalActions + newSSEAlerts.length,
+ };
+
+ return merged;
+ }, [actionQueue, notificationKey]); // Use notificationKey instead of notifications to prevent infinite re-nders
+
+ // Use merged data if available, otherwise use original API data
+ const displayQueue = mergedActionQueue || actionQueue;
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!displayQueue || displayQueue.totalActions === 0) {
+ return (
+
+
+
+
+
+ {t('dashboard:all_caught_up')}
+
+
+ {t('dashboard:no_actions_needed')}
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {t('dashboard:action_queue_title')}
+
+ {/* SSE Connection Indicator */}
+
+ {isConnected ? (
+ <>
+
+ Live
+ >
+ ) : (
+ <>
+
+ Offline
+ >
+ )}
+
+
+
5 ? 'var(--color-error-100)' : 'var(--color-info-100)',
+ color: displayQueue.totalActions > 5 ? 'var(--color-error-800)' : 'var(--color-info-800)',
+ }}
+ >
+ {displayQueue.totalActions} {t('dashboard:total_actions')}
+
+
+
+ {/* Toast Notification */}
+ {toastMessage && (
+
+ {toastMessage.type === 'success' ? (
+
+ ) : (
+
+ )}
+ {toastMessage.message}
+
+ )}
+
+ {/* URGENT Section (<6h) */}
+
+
+ {/* TODAY Section (<24h) */}
+
+
+ {/* THIS WEEK Section (<7d) */}
+
+
+ {/* Stock Receipt Modal - Opened by delivery actions */}
+ {isStockReceiptModalOpen && stockReceiptData && (
+
{
+ setIsStockReceiptModalOpen(false);
+ setStockReceiptData(null);
+ }}
+ receipt={stockReceiptData.receipt}
+ mode={stockReceiptData.mode}
+ onSaveDraft={async (receipt) => {
+ console.log('Draft saved:', receipt);
+ // TODO: Implement save draft API call
+ }}
+ onConfirm={async (receipt) => {
+ console.log('Receipt confirmed:', receipt);
+ // TODO: Implement confirm receipt API call
+ setIsStockReceiptModalOpen(false);
+ setStockReceiptData(null);
+ }}
+ />
+ )}
+
+ {/* Reasoning Modal - Opened by "See Full Reasoning" action */}
+ {reasoningModalOpen && reasoningData && (
+ {
+ setReasoningModalOpen(false);
+ setReasoningData(null);
+ }}
+ reasoning={reasoningData}
+ />
+ )}
+
+ );
+}
diff --git a/frontend/src/components/domain/dashboard/ReasoningModal.tsx b/frontend/src/components/domain/dashboard/ReasoningModal.tsx
new file mode 100644
index 00000000..5f5b00f8
--- /dev/null
+++ b/frontend/src/components/domain/dashboard/ReasoningModal.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import { X, Brain, TrendingUp, Clock, AlertCircle } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+interface ReasoningModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ reasoning: {
+ action_id?: string;
+ po_id?: string;
+ batch_id?: string;
+ reasoning?: string;
+ title?: string;
+ ai_reasoning_summary?: string;
+ business_impact?: {
+ financial_impact_eur?: number;
+ affected_orders?: number;
+ };
+ urgency_context?: {
+ deadline?: string;
+ time_until_consequence_hours?: number;
+ };
+ };
+}
+
+export function ReasoningModal({ isOpen, onClose, reasoning }: ReasoningModalProps) {
+ const { t } = useTranslation(['alerts', 'common']);
+
+ if (!isOpen) return null;
+
+ const displayText = reasoning.ai_reasoning_summary || reasoning.reasoning || t('alerts:no_reasoning_available');
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {reasoning.title || t('alerts:orchestration.reasoning_title')}
+
+
+ {t('common:ai_reasoning')}
+
+
+
+
+
+
+ {/* Content */}
+
+ {/* AI Reasoning Summary */}
+
+
+ {/* Business Impact (if available) */}
+ {reasoning.business_impact && (
+
+
+ {t('alerts:business_impact')}
+
+
+ {reasoning.business_impact.financial_impact_eur !== undefined && (
+
+
+
+
+ {t('alerts:context.financial_impact', { amount: reasoning.business_impact.financial_impact_eur.toFixed(0) })}
+
+
+
+ )}
+ {reasoning.business_impact.affected_orders !== undefined && (
+
+
+
+
+ {reasoning.business_impact.affected_orders} {t('common:orders_affected')}
+
+
+
+ )}
+
+
+ )}
+
+ {/* Urgency Context (if available) */}
+ {reasoning.urgency_context?.time_until_consequence_hours !== undefined && (
+
+
+
+
+ {Math.round(reasoning.urgency_context.time_until_consequence_hours)}h {t('common:remaining')}
+
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/domain/dashboard/index.ts b/frontend/src/components/domain/dashboard/index.ts
index a587adfd..679ff372 100644
--- a/frontend/src/components/domain/dashboard/index.ts
+++ b/frontend/src/components/domain/dashboard/index.ts
@@ -1,16 +1,17 @@
// Dashboard Domain Components - Bakery Management System
// Existing dashboard components
-export { default as RealTimeAlerts } from './RealTimeAlerts';
export { default as PendingPOApprovals } from './PendingPOApprovals';
export { default as TodayProduction } from './TodayProduction';
-export { default as AlertTrends } from './AlertTrends';
// Production Management Dashboard Widgets
export { default as ProductionCostMonitor } from './ProductionCostMonitor';
export { default as EquipmentStatusWidget } from './EquipmentStatusWidget';
export { default as AIInsightsWidget } from './AIInsightsWidget';
+// Reasoning Modal
+export { ReasoningModal } from './ReasoningModal';
+
// Types
export type { ProductionCostData } from './ProductionCostMonitor';
export type { EquipmentStatus } from './EquipmentStatusWidget';