feat(dashboard): Add reasoning modal for action buttons

- Create ReasoningModal component with AI reasoning display
- Add business impact section (financial impact, affected orders)
- Add urgency context display (time remaining)
- Wire up 'reasoning:show' event listener in UnifiedActionQueueCard
- Export ReasoningModal from domain/dashboard index

The smartActionHandlers already emit the 'reasoning:show' event,
this adds the missing listener and modal to display the AI reasoning
when users click "See Full Reasoning" on alerts.

Fixes: Issue #2 - "See Full Reasoning" button not working

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Urtzi Alfaro
2025-11-27 07:36:06 +01:00
parent 035b45a0a6
commit b2bde32502
3 changed files with 903 additions and 2 deletions

View File

@@ -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 (
<div
className="flex items-center gap-1 px-2 py-1 rounded-md text-xs font-semibold"
style={{
backgroundColor: 'var(--color-warning-100)',
color: 'var(--color-warning-900)',
}}
>
<AlertTriangle className="w-3 h-3" />
<span>
{t('escalated')} +{escalation.boost_applied}
{hoursPending && ` (${hoursPending}h pending)`}
</span>
</div>
);
}
function ActionCard({ alert, showEscalationBadge = false, onActionSuccess, onActionError }: ActionCardProps) {
const [expanded, setExpanded] = useState(false);
const [loadingAction, setLoadingAction] = useState<string | null>(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 (
<div
className={`border-2 rounded-lg p-4 transition-all duration-200 hover:shadow-md ${
actionCompleted ? 'opacity-60' : ''
} ${isCritical ? 'shadow-md' : ''}`}
style={{
backgroundColor: colors.bg,
borderColor: colors.border,
borderWidth: isCritical ? '3px' : '2px',
}}
>
{/* Header */}
<div className="flex items-start gap-3 mb-3">
<AlertIcon
className={`flex-shrink-0 mt-1 ${isCritical ? 'w-8 h-8' : 'w-7 h-7'}`}
style={{ color: colors.border }}
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-2">
<h3 className={`font-bold ${isCritical ? 'text-xl' : 'text-lg'}`} style={{ color: colors.text }}>
{alert.title}
</h3>
{/* Only show escalation badge if applicable */}
{showEscalationBadge && alert.alert_metadata?.escalation && (
<div className="flex items-center gap-2 flex-shrink-0">
<span
className="px-2 py-1 rounded text-xs font-semibold uppercase"
style={{
backgroundColor: 'var(--color-error-100)',
color: 'var(--color-error-800)',
}}
>
AGED
</span>
</div>
)}
</div>
{/* Escalation Badge Details */}
{showEscalationBadge && <EscalationBadge alert={alert} />}
{/* Message */}
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary)' }}>
{alert.message}
</p>
{/* Context Badges - Matching Health Hero Style */}
{(alert.business_impact || alert.urgency_context || alert.type_class === 'prevented_issue') && (
<div className="flex flex-wrap gap-2 mb-3">
{alert.business_impact?.financial_impact_eur && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-semibold"
style={{
backgroundColor: 'var(--color-warning-100)',
color: 'var(--color-warning-800)',
}}
>
<TrendingUp className="w-4 h-4" />
<span>{alert.business_impact.financial_impact_eur.toFixed(0)} at risk</span>
</div>
)}
{alert.urgency_context?.time_until_consequence_hours && (
<div
className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-semibold ${
alert.urgency_context.time_until_consequence_hours < 6 ? 'animate-pulse' : ''
}`}
style={{
backgroundColor: 'var(--color-error-100)',
color: 'var(--color-error-800)',
}}
>
<Clock className="w-4 h-4" />
<span>{Math.round(alert.urgency_context.time_until_consequence_hours)}h left</span>
</div>
)}
{alert.type_class === 'prevented_issue' && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-semibold"
style={{
backgroundColor: 'var(--color-success-100)',
color: 'var(--color-success-800)',
}}
>
<Bot className="w-4 h-4" />
<span>AI handled</span>
</div>
)}
</div>
)}
{/* AI Reasoning (expandable) */}
{alert.ai_reasoning_summary && (
<>
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 text-sm font-medium transition-colors mb-2"
style={{ color: 'var(--color-info-700)' }}
>
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
<Zap className="w-4 h-4" />
<span>AI Reasoning</span>
</button>
{expanded && (
<div
className="rounded-md p-3 mb-3"
style={{
backgroundColor: 'var(--bg-secondary)',
borderLeft: '3px solid var(--color-info-600)',
}}
>
<div className="flex items-start gap-2">
<Bot className="w-4 h-4 flex-shrink-0 mt-0.5" style={{ color: 'var(--color-info-700)' }} />
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{alert.ai_reasoning_summary}
</p>
</div>
</div>
)}
</>
)}
{/* Smart Actions - Improved with loading states and icons */}
{alertActions.length > 0 && !actionCompleted && (
<div className="flex flex-wrap gap-2 mt-3 sm:flex-row flex-col sm:items-center">
{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 (
<Button
key={idx}
variant={buttonVariant}
size={isPrimary ? 'md' : 'sm'}
isLoading={isLoading}
disabled={action.disabled || loadingAction !== null}
leftIcon={ActionIcon && !isLoading ? <ActionIcon className="w-4 h-4" /> : 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 && (
<span className="ml-1 opacity-60 text-xs">({action.estimated_time_minutes}m)</span>
)}
</Button>
);
})}
</div>
)}
{/* Action Completed State */}
{actionCompleted && (
<div
className="flex items-center gap-2 mt-3 px-3 py-2 rounded-md"
style={{
backgroundColor: 'var(--color-success-100)',
color: 'var(--color-success-800)',
}}
>
<CheckCircle className="w-5 h-5" />
<span className="font-semibold">Action completed successfully</span>
</div>
)}
</div>
</div>
</div>
);
}
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 (
<div className="mb-6">
{/* Section Header */}
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center justify-between w-full mb-4 group"
>
<div className="flex items-center gap-2">
<Icon className="w-5 h-5" style={{ color: iconColor }} />
<h3 className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
{title}
</h3>
<span
className="px-2 py-1 rounded-full text-xs font-bold"
style={{
backgroundColor: iconColor.replace('700', '100'),
color: iconColor,
}}
>
{count}
</span>
</div>
{expanded ? (
<ChevronUp className="w-5 h-5 transition-transform" style={{ color: 'var(--text-secondary)' }} />
) : (
<ChevronDown className="w-5 h-5 transition-transform" style={{ color: 'var(--text-secondary)' }} />
)}
</button>
{/* Section Content */}
{expanded && (
<div className="space-y-3">
{alerts.map((alert) => (
<ActionCard
key={alert.id}
alert={alert}
showEscalationBadge={showEscalationBadges}
onActionSuccess={onActionSuccess}
onActionError={onActionError}
/>
))}
</div>
)}
</div>
);
}
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<any>(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 (
<div
className="rounded-xl shadow-lg p-6 border"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border-primary)',
}}
>
<div className="animate-pulse space-y-4">
<div className="h-6 rounded w-1/2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-32 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
<div className="h-32 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div>
</div>
</div>
);
}
if (!displayQueue || displayQueue.totalActions === 0) {
return (
<div
className="border-2 rounded-xl p-8 text-center shadow-lg"
style={{
backgroundColor: 'var(--color-success-50)',
borderColor: 'var(--color-success-200)',
}}
>
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--color-success-100)' }}>
<Zap className="w-8 h-8" style={{ color: 'var(--color-success-600)' }} />
</div>
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--color-success-900)' }}>
{t('dashboard:all_caught_up')}
</h3>
<p style={{ color: 'var(--color-success-700)' }}>
{t('dashboard:no_actions_needed')}
</p>
</div>
);
}
return (
<div
className="rounded-xl shadow-lg p-6 border"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border-primary)',
}}
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{t('dashboard:action_queue_title')}
</h2>
{/* SSE Connection Indicator */}
<div className="flex items-center gap-1.5 text-xs" style={{ color: 'var(--text-secondary)' }}>
{isConnected ? (
<>
<Wifi className="w-3.5 h-3.5" style={{ color: 'var(--color-success)' }} />
<span className="hidden sm:inline">Live</span>
</>
) : (
<>
<WifiOff className="w-3.5 h-3.5" style={{ color: 'var(--color-error)' }} />
<span className="hidden sm:inline">Offline</span>
</>
)}
</div>
</div>
<span
className="px-3 py-1 rounded-full text-sm font-semibold"
style={{
backgroundColor: displayQueue.totalActions > 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')}
</span>
</div>
{/* Toast Notification */}
{toastMessage && (
<div
className="fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 animate-slide-in-right"
style={{
backgroundColor: toastMessage.type === 'success' ? 'var(--color-success-600)' : 'var(--color-error-600)',
color: 'white',
}}
>
{toastMessage.type === 'success' ? (
<CheckCircle className="w-5 h-5" />
) : (
<XCircle className="w-5 h-5" />
)}
<span className="font-medium">{toastMessage.message}</span>
</div>
)}
{/* URGENT Section (<6h) */}
<ActionSection
title="🔴 URGENT - Next 6 Hours"
icon={AlertCircle}
iconColor="var(--color-error-700)"
alerts={displayQueue.urgent}
count={displayQueue.urgentCount}
defaultExpanded={true}
showEscalationBadges={true}
onActionSuccess={handleActionSuccess}
onActionError={handleActionError}
/>
{/* TODAY Section (<24h) */}
<ActionSection
title="🟡 TODAY - Next 24 Hours"
icon={Clock}
iconColor="var(--color-warning-700)"
alerts={displayQueue.today}
count={displayQueue.todayCount}
defaultExpanded={true}
showEscalationBadges={true}
onActionSuccess={handleActionSuccess}
onActionError={handleActionError}
/>
{/* THIS WEEK Section (<7d) */}
<ActionSection
title="🟢 THIS WEEK"
icon={Calendar}
iconColor="var(--color-success-700)"
alerts={displayQueue.week}
count={displayQueue.weekCount}
defaultExpanded={false}
showEscalationBadges={true}
onActionSuccess={handleActionSuccess}
onActionError={handleActionError}
/>
{/* Stock Receipt Modal - Opened by delivery actions */}
{isStockReceiptModalOpen && stockReceiptData && (
<StockReceiptModal
isOpen={isStockReceiptModalOpen}
onClose={() => {
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 && (
<ReasoningModal
isOpen={reasoningModalOpen}
onClose={() => {
setReasoningModalOpen(false);
setReasoningData(null);
}}
reasoning={reasoningData}
/>
)}
</div>
);
}

View File

@@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div
className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-y-auto"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
{/* Header */}
<div className="sticky top-0 flex items-center justify-between p-6 border-b" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--color-info-100)' }}>
<Brain className="w-6 h-6" style={{ color: 'var(--color-info-600)' }} />
</div>
<div>
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{reasoning.title || t('alerts:orchestration.reasoning_title')}
</h2>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{t('common:ai_reasoning')}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-black/5 transition-colors"
>
<X className="w-6 h-6" style={{ color: 'var(--text-secondary)' }} />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* AI Reasoning Summary */}
<div className="rounded-lg p-4 border-l-4" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--color-info-600)' }}>
<p className="text-base leading-relaxed" style={{ color: 'var(--text-primary)' }}>
{displayText}
</p>
</div>
{/* Business Impact (if available) */}
{reasoning.business_impact && (
<div className="space-y-2">
<h3 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>
{t('alerts:business_impact')}
</h3>
<div className="grid grid-cols-2 gap-3">
{reasoning.business_impact.financial_impact_eur !== undefined && (
<div className="rounded-lg p-3" style={{ backgroundColor: 'var(--color-warning-50)' }}>
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4" style={{ color: 'var(--color-warning-600)' }} />
<span className="text-xs font-semibold" style={{ color: 'var(--color-warning-800)' }}>
{t('alerts:context.financial_impact', { amount: reasoning.business_impact.financial_impact_eur.toFixed(0) })}
</span>
</div>
</div>
)}
{reasoning.business_impact.affected_orders !== undefined && (
<div className="rounded-lg p-3" style={{ backgroundColor: 'var(--color-info-50)' }}>
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-4 h-4" style={{ color: 'var(--color-info-600)' }} />
<span className="text-xs font-semibold" style={{ color: 'var(--color-info-800)' }}>
{reasoning.business_impact.affected_orders} {t('common:orders_affected')}
</span>
</div>
</div>
)}
</div>
</div>
)}
{/* Urgency Context (if available) */}
{reasoning.urgency_context?.time_until_consequence_hours !== undefined && (
<div className="rounded-lg p-4" style={{ backgroundColor: 'var(--color-error-50)', borderLeft: '4px solid var(--color-error-600)' }}>
<div className="flex items-center gap-2">
<Clock className="w-5 h-5" style={{ color: 'var(--color-error-600)' }} />
<span className="font-semibold" style={{ color: 'var(--color-error-800)' }}>
{Math.round(reasoning.urgency_context.time_until_consequence_hours)}h {t('common:remaining')}
</span>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="sticky bottom-0 p-6 border-t" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
<button
onClick={onClose}
className="w-full px-6 py-3 rounded-lg font-semibold transition-colors"
style={{ backgroundColor: 'var(--color-primary)', color: 'white' }}
>
{t('common:close')}
</button>
</div>
</div>
</div>
);
}

View File

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